controller/MailMutationPipeline.cpp
| Line | Branch | Exec | Source |
|---|---|---|---|
| 1 | #include "controller/MailMutationPipeline.h" | ||
| 2 | |||
| 3 | #include <QDeadlineTimer> | ||
| 4 | #include <QFutureWatcher> | ||
| 5 | #include <QLoggingCategory> | ||
| 6 | #include <QPointer> | ||
| 7 | #include <QtConcurrentRun> | ||
| 8 | #include <algorithm> | ||
| 9 | #include <limits> | ||
| 10 | #include <utility> | ||
| 11 | #include "data/MailCache.h" | ||
| 12 | #include "data/Models.h" | ||
| 13 | #include "performance/MutationPerformanceTracker.h" | ||
| 14 | #include "service/ImapService.h" | ||
| 15 | #include "service/NetworkLifecycleCoordinator.h" | ||
| 16 | #include "ui/MailListModel.h" | ||
| 17 | #include "ui/MailThreadModel.h" | ||
| 18 | #include "util/MessageId.h" | ||
| 19 | |||
| 20 | // Same category as MailController (defined there), see docs/LOGGING.md. | ||
| 21 | Q_DECLARE_LOGGING_CATEGORY(lcController) | ||
| 22 | |||
| 23 | 1050 | MailMutationPipeline::MailMutationPipeline( | |
| 24 | ImapService *imap, ImapService *const &bodyImap, MailCache *cache, MailListModel *model, | ||
| 25 | MailThreadModel *const &threadModel, const FolderContext &folder, | ||
| 26 | const ImapConfig &imapConfig, const QSet<QString> &unavailableFolders, Deps deps, | ||
| 27 | 1050 | QObject *parent) | |
| 28 | 1050 | : QObject(parent), m_imap(imap), m_bodyImap(bodyImap), m_cache(cache), m_model(model), | |
| 29 | 1050 | m_threadModel(threadModel), m_folder(folder), m_imapConfig(imapConfig), | |
| 30 |
2/2✓ Branch 13 → 14 taken 1050 times.
✓ Branch 14 → 15 taken 1050 times.
|
1050 | m_unavailableFolders(unavailableFolders), m_deps(std::move(deps)) { |
| 31 |
1/1✓ Branch 17 → 18 taken 1050 times.
|
1050 | m_appendPayloadPool.setMaxThreadCount(1); |
| 32 |
1/1✓ Branch 18 → 19 taken 1050 times.
|
1050 | m_appendWritePool.setMaxThreadCount(1); |
| 33 | 1050 | } | |
| 34 | |||
| 35 | // Mutation consumers of the main connection, connected by the controller | ||
| 36 | // constructor at their former position (after the move-result slots). | ||
| 37 | 1050 | void MailMutationPipeline::connectMainConnection(ImapService *imap) { | |
| 38 | 1050 | connect(imap, &ImapService::flagSnapshotsReceived, this, | |
| 39 |
1/1✓ Branch 2 → 3 taken 1050 times.
|
1050 | &MailMutationPipeline::onFlagSnapshotsReceived); |
| 40 | 1050 | connect(imap, &ImapService::verifyFlagsReceived, this, | |
| 41 |
1/1✓ Branch 4 → 5 taken 1050 times.
|
1050 | &MailMutationPipeline::onVerificationFlagsReceived); |
| 42 | 1050 | connect(imap, &ImapService::mutationDispatched, this, | |
| 43 |
1/1✓ Branch 6 → 7 taken 1050 times.
|
1050 | &MailMutationPipeline::onMutationDispatched); |
| 44 | 1050 | connect(imap, &ImapService::mutationCompleted, this, | |
| 45 |
1/1✓ Branch 8 → 9 taken 1050 times.
|
1050 | [this](qint64 token, ImapService::MutationOutcome outcome, |
| 46 | const QString &error, const QString &uidPlus) { | ||
| 47 | 12 | onMutationCompleted(token, static_cast<int>(outcome), error, | |
| 48 | uidPlus); | ||
| 49 | 12 | }); | |
| 50 | 1050 | connect(imap, &ImapService::stateChanged, this, | |
| 51 |
1/1✓ Branch 10 → 11 taken 1050 times.
|
1050 | [this](ImapService::State state) { |
| 52 |
4/4✓ Branch 2 → 3 taken 62 times.
✓ Branch 2 → 4 taken 2 times.
✓ Branch 3 → 4 taken 5 times.
✓ Branch 3 → 19 taken 57 times.
|
64 | if (state == ImapService::State::Disconnected || |
| 53 | state == ImapService::State::Error) { | ||
| 54 | 7 | cancelPendingAppendPayloadReads(m_imap); | |
| 55 | 7 | m_mutationsScheduledForDispatch.clear(); | |
| 56 | 7 | for (auto it = m_appendPayloadReads.cbegin(); | |
| 57 |
1/2✗ Branch 12 → 7 not taken.
✓ Branch 12 → 13 taken 7 times.
|
7 | it != m_appendPayloadReads.cend(); ++it) |
| 58 | ✗ | m_mutationsScheduledForDispatch.insert(it.key()); | |
| 59 | 7 | m_mutationsScheduledForVerification.clear(); | |
| 60 | 7 | m_deferredMutationVerifications.clear(); | |
| 61 | 7 | m_mutationVerificationAttempts.clear(); | |
| 62 | 7 | m_resentMutations.clear(); | |
| 63 | 7 | m_slowVerificationReports.clear(); | |
| 64 | 7 | ++m_mutationVerificationGeneration; | |
| 65 | } | ||
| 66 |
2/2✓ Branch 19 → 20 taken 9 times.
✓ Branch 19 → 22 taken 55 times.
|
64 | if (state == ImapService::State::Authenticated) |
| 67 | 9 | QTimer::singleShot(0, this, | |
| 68 |
1/1✓ Branch 20 → 21 taken 9 times.
|
9 | &MailMutationPipeline::replayMutationJournal); |
| 69 | 64 | }); | |
| 70 | 1050 | } | |
| 71 | |||
| 72 | // Journal removals retire the token's verification state and pending action. | ||
| 73 | 1050 | void MailMutationPipeline::connectCacheSignals() { | |
| 74 |
1/1✓ Branch 2 → 3 taken 1050 times.
|
1050 | connect(m_cache, &MailCache::mutationRemoved, this, [this](qint64 token) { |
| 75 | 64 | m_slowVerificationReports.remove(token); | |
| 76 | 64 | m_mutationsScheduledForVerification.remove(token); | |
| 77 | 64 | m_deferredMutationVerifications.remove(token); | |
| 78 | 64 | m_mutationVerificationAttempts.remove(token); | |
| 79 | 64 | m_resentMutations.remove(token); | |
| 80 |
1/1✓ Branch 8 → 9 taken 64 times.
|
64 | m_deps.pendingActionChanged(token, {}); |
| 81 | 64 | }); | |
| 82 | 1050 | } | |
| 83 | |||
| 84 | 1043 | void MailMutationPipeline::resetForAccountSwitch() { | |
| 85 | 1043 | ++m_mutationVerificationGeneration; | |
| 86 | 1043 | m_mutationsScheduledForVerification.clear(); | |
| 87 | 1043 | m_deferredMutationVerifications.clear(); | |
| 88 | 1043 | m_mutationVerificationAttempts.clear(); | |
| 89 | 1043 | m_resentMutations.clear(); | |
| 90 | 1043 | m_slowVerificationReports.clear(); | |
| 91 | 1043 | m_ambiguousReconciliationQueue.clear(); | |
| 92 |
1/2✗ Branch 8 → 9 not taken.
✓ Branch 8 → 10 taken 1043 times.
|
1043 | if (m_activeReconciliationToken != 0) finishAmbiguousReconciliation(); |
| 93 | 1043 | } | |
| 94 | |||
| 95 | 1050 | void MailMutationPipeline::shutdown() { | |
| 96 | 1050 | cancelPendingAppendPayloadReads(); | |
| 97 |
1/1✓ Branch 4 → 5 taken 1050 times.
|
1050 | m_appendPayloadPool.waitForDone(); |
| 98 |
1/1✓ Branch 6 → 7 taken 1050 times.
|
1050 | m_appendWritePool.waitForDone(); |
| 99 | 1050 | } | |
| 100 | |||
| 101 | // S91.0.6: mutation dispatch/verification consumers of the body connection. | ||
| 102 | 19 | void MailMutationPipeline::connectMutationVerification(ImapService *bodyImap) { | |
| 103 | 19 | connect(bodyImap, &ImapService::flagSnapshotsReceived, this, | |
| 104 |
1/1✓ Branch 2 → 3 taken 19 times.
|
19 | &MailMutationPipeline::onFlagSnapshotsReceived); |
| 105 | 19 | connect(bodyImap, &ImapService::verifyFlagsReceived, this, | |
| 106 |
1/1✓ Branch 4 → 5 taken 19 times.
|
19 | &MailMutationPipeline::onVerificationFlagsReceived); |
| 107 | 19 | connect(bodyImap, &ImapService::mutationDispatched, this, | |
| 108 |
1/1✓ Branch 6 → 7 taken 19 times.
|
19 | &MailMutationPipeline::onMutationDispatched); |
| 109 | 19 | connect(bodyImap, &ImapService::mutationCompleted, this, | |
| 110 |
1/1✓ Branch 8 → 9 taken 19 times.
|
19 | [this](qint64 token, ImapService::MutationOutcome outcome, |
| 111 | const QString &error, const QString &uidPlus) { | ||
| 112 | ✗ | onMutationCompleted(token, static_cast<int>(outcome), error, | |
| 113 | uidPlus); | ||
| 114 | ✗ | }); | |
| 115 | 19 | connect(bodyImap, &ImapService::stateChanged, this, | |
| 116 |
1/1✓ Branch 10 → 11 taken 19 times.
|
19 | [this, bodyImap](ImapService::State s) { |
| 117 |
3/4✓ Branch 2 → 3 taken 3 times.
✓ Branch 2 → 4 taken 1 time.
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 20 taken 3 times.
|
4 | if (s == ImapService::State::Disconnected || |
| 118 | s == ImapService::State::Error) { | ||
| 119 | 1 | cancelPendingAppendPayloadReads(bodyImap); | |
| 120 | 1 | m_mutationsScheduledForDispatch.clear(); | |
| 121 | 1 | for (auto it = m_appendPayloadReads.cbegin(); | |
| 122 |
1/2✗ Branch 12 → 7 not taken.
✓ Branch 12 → 13 taken 1 time.
|
1 | it != m_appendPayloadReads.cend(); ++it) |
| 123 | ✗ | m_mutationsScheduledForDispatch.insert(it.key()); | |
| 124 | 1 | m_mutationsScheduledForVerification.clear(); | |
| 125 | 1 | m_deferredMutationVerifications.clear(); | |
| 126 | 1 | m_mutationVerificationAttempts.clear(); | |
| 127 | 1 | m_resentMutations.clear(); | |
| 128 | 1 | m_slowVerificationReports.clear(); | |
| 129 | 1 | ++m_mutationVerificationGeneration; | |
| 130 |
1/2✗ Branch 18 → 19 not taken.
✓ Branch 18 → 20 taken 1 time.
|
1 | if (m_activeReconciliationToken != 0) |
| 131 | ✗ | finishAmbiguousReconciliation(); | |
| 132 | } | ||
| 133 |
2/2✓ Branch 20 → 21 taken 2 times.
✓ Branch 20 → 22 taken 2 times.
|
4 | if (s == ImapService::State::Authenticated) |
| 134 | 2 | replayMutationJournal(); | |
| 135 | 4 | }); | |
| 136 | 19 | } | |
| 137 | |||
| 138 | 90 | qint64 MailMutationPipeline::persistDesiredMutation(qint64 folderId, qint64 uid, | |
| 139 | MutationKind kind, | ||
| 140 | const QString &flag, | ||
| 141 | bool desiredState) { | ||
| 142 |
1/1✓ Branch 2 → 3 taken 90 times.
|
90 | const QString folderPath = m_cache->folderPath(folderId); |
| 143 |
1/1✓ Branch 3 → 4 taken 90 times.
|
90 | const auto header = m_cache->header(folderId, uid); |
| 144 |
8/8✓ Branch 5 → 6 taken 89 times.
✓ Branch 5 → 10 taken 1 time.
✓ Branch 7 → 8 taken 88 times.
✓ Branch 7 → 10 taken 1 time.
✓ Branch 9 → 10 taken 1 time.
✓ Branch 9 → 11 taken 87 times.
✓ Branch 12 → 13 taken 3 times.
✓ Branch 12 → 14 taken 87 times.
|
90 | if (m_folder.accountId.isEmpty() || folderPath.isEmpty() || !header) |
| 145 | 3 | return 0; | |
| 146 | |||
| 147 | 87 | MutationRecord record; | |
| 148 | 87 | record.mailId = MailId{m_folder.accountId, folderId, uid}; | |
| 149 | 87 | record.folderPath = folderPath; | |
| 150 | 87 | record.kind = kind; | |
| 151 | 87 | record.replayClass = MutationReplayClass::DesiredState; | |
| 152 | 87 | record.phase = MutationPhase::Queued; | |
| 153 | 87 | record.flag = flag; | |
| 154 | 87 | record.desiredState = desiredState; | |
| 155 | 87 | record.messageId = header->messageId; | |
| 156 |
1/1✓ Branch 21 → 22 taken 87 times.
|
87 | record.uidValidity = m_cache->uidValidity(folderId); |
| 157 | // S106-184 (INV-13/15): a record without the mailbox epoch would run | ||
| 158 | // unfenced against whatever mailbox the UID names after a reset. A | ||
| 159 | // folder never opened on the server has no confirmed epoch yet; labels | ||
| 160 | // and the selection writers refuse it the same way. | ||
| 161 |
2/2✓ Branch 22 → 23 taken 2 times.
✓ Branch 22 → 39 taken 85 times.
|
87 | if (record.uidValidity == 0) { |
| 162 |
6/6✓ Branch 23 → 24 taken 2 times.
✓ Branch 27 → 28 taken 2 times.
✓ Branch 28 → 29 taken 2 times.
✓ Branch 29 → 30 taken 2 times.
✓ Branch 34 → 25 taken 2 times.
✓ Branch 34 → 35 taken 2 times.
|
4 | qCWarning(lcController) << "Desired state not queued: folder" << folderId |
| 163 |
1/1✓ Branch 30 → 31 taken 2 times.
|
2 | << "has no confirmed UIDVALIDITY"; |
| 164 |
2/2✓ Branch 35 → 36 taken 2 times.
✓ Branch 36 → 37 taken 2 times.
|
2 | m_deps.statusMessage(tr("Action not queued: open the folder once so its server identity is known.")); |
| 165 | 2 | return 0; | |
| 166 | } | ||
| 167 | auto &performanceTracker = | ||
| 168 |
1/1✓ Branch 39 → 40 taken 85 times.
|
85 | MailJD::Performance::MutationPerformanceTracker::instance(); |
| 169 |
1/1✓ Branch 40 → 41 taken 85 times.
|
85 | performanceTracker.journalStarted(); |
| 170 |
1/1✓ Branch 42 → 43 taken 85 times.
|
85 | const auto token = m_cache->journal().enqueueMutation(record); |
| 171 |
1/2✗ Branch 44 → 45 not taken.
✓ Branch 44 → 68 taken 85 times.
|
85 | if (!token) { |
| 172 | ✗ | performanceTracker.journalFinished(false); | |
| 173 | ✗ | qCWarning(lcController) << "Mutation journal rejected desired state:" | |
| 174 | ✗ | << m_cache->lastError(); | |
| 175 | ✗ | m_deps.statusMessage(tr("Action not queued: %1").arg(m_cache->lastError())); | |
| 176 | ✗ | return 0; | |
| 177 | } | ||
| 178 |
1/1✓ Branch 69 → 70 taken 85 times.
|
85 | performanceTracker.journalFinished(true, *token); |
| 179 | 85 | return *token; | |
| 180 | 90 | } | |
| 181 | |||
| 182 | 4 | bool MailMutationPipeline::retryAppendMutation(qint64 token) { | |
| 183 |
1/1✓ Branch 3 → 4 taken 4 times.
|
4 | const auto record = m_cache->journal().mutation(token); |
| 184 |
3/4✓ Branch 7 → 8 taken 4 times.
✗ Branch 7 → 16 not taken.
✓ Branch 10 → 11 taken 2 times.
✓ Branch 10 → 16 taken 2 times.
|
8 | if (!record || record->kind != MutationKind::Append || record->mailId.accountId != m_folder.accountId || |
| 185 |
5/6✓ Branch 5 → 6 taken 4 times.
✗ Branch 5 → 16 not taken.
✓ Branch 12 → 13 taken 1 time.
✓ Branch 12 → 16 taken 1 time.
✓ Branch 18 → 19 taken 3 times.
✓ Branch 18 → 20 taken 1 time.
|
9 | record->phase != MutationPhase::Failed || |
| 186 |
2/3✓ Branch 14 → 15 taken 1 time.
✗ Branch 15 → 16 not taken.
✓ Branch 15 → 17 taken 1 time.
|
4 | !m_cache->journal().requeueFailedAppend(token, m_folder.accountId)) return false; |
| 187 |
1/1✓ Branch 20 → 21 taken 1 time.
|
1 | QTimer::singleShot(0, this, &MailMutationPipeline::replayMutationJournal); |
| 188 | 1 | return true; | |
| 189 | 4 | } | |
| 190 | |||
| 191 | 13 | qint64 MailMutationPipeline::queueAppend(const QString &folderPath, | |
| 192 | const QByteArray &rfcMessage, | ||
| 193 | const QString &flags, | ||
| 194 | const QString &messageId, | ||
| 195 | const QByteArray &precomputedSha256) { | ||
| 196 |
1/1✓ Branch 2 → 3 taken 13 times.
|
13 | const QString canonicalMessageId = MessageId::canonicalFirst(messageId); |
| 197 |
10/10✓ Branch 4 → 5 taken 12 times.
✓ Branch 4 → 11 taken 1 time.
✓ Branch 6 → 7 taken 11 times.
✓ Branch 6 → 11 taken 1 time.
✓ Branch 8 → 9 taken 10 times.
✓ Branch 8 → 11 taken 1 time.
✓ Branch 10 → 11 taken 1 time.
✓ Branch 10 → 12 taken 9 times.
✓ Branch 13 → 14 taken 4 times.
✓ Branch 13 → 18 taken 9 times.
|
23 | if (m_folder.accountId.isEmpty() || folderPath.isEmpty() || rfcMessage.isEmpty() || |
| 198 | 10 | canonicalMessageId.isEmpty()) { | |
| 199 |
2/2✓ Branch 14 → 15 taken 4 times.
✓ Branch 15 → 16 taken 4 times.
|
4 | m_deps.statusMessage(tr("Message copy not queued: invalid append metadata")); |
| 200 | 4 | return 0; | |
| 201 | } | ||
| 202 | |||
| 203 | 9 | MutationRecord record; | |
| 204 | // APPEND creates its server MailId. Folder/UID zero deliberately express | ||
| 205 | // that no existing server object may be targeted or inferred. | ||
| 206 | 9 | record.mailId = MailId{m_folder.accountId, 0, 0}; | |
| 207 | 9 | record.folderPath = folderPath; | |
| 208 | 9 | record.kind = MutationKind::Append; | |
| 209 | 9 | record.replayClass = MutationReplayClass::AmbiguousOperation; | |
| 210 | 9 | record.phase = MutationPhase::Queued; | |
| 211 | 9 | record.targetFolder = folderPath; | |
| 212 | 9 | record.messageId = canonicalMessageId; | |
| 213 |
1/1✓ Branch 25 → 26 taken 9 times.
|
9 | const auto token = m_cache->journal().enqueueAppendMutation(record, rfcMessage, flags, |
| 214 | precomputedSha256); | ||
| 215 |
2/2✓ Branch 27 → 28 taken 1 time.
✓ Branch 27 → 50 taken 8 times.
|
9 | if (!token) { |
| 216 |
5/5✓ Branch 28 → 29 taken 1 time.
✓ Branch 32 → 33 taken 1 time.
✓ Branch 33 → 34 taken 1 time.
✓ Branch 40 → 30 taken 1 time.
✓ Branch 40 → 41 taken 1 time.
|
2 | qCWarning(lcController) << "Mutation journal rejected APPEND:" |
| 217 |
2/2✓ Branch 34 → 35 taken 1 time.
✓ Branch 35 → 36 taken 1 time.
|
1 | << m_cache->lastError(); |
| 218 |
1/1✓ Branch 45 → 46 taken 1 time.
|
1 | m_deps.statusMessage( |
| 219 |
3/3✓ Branch 41 → 42 taken 1 time.
✓ Branch 43 → 44 taken 1 time.
✓ Branch 44 → 45 taken 1 time.
|
3 | tr("Message copy not queued: %1").arg(m_cache->lastError())); |
| 220 | 1 | return 0; | |
| 221 | } | ||
| 222 | |||
| 223 |
1/1✓ Branch 51 → 52 taken 8 times.
|
8 | QTimer::singleShot(0, this, [this, token = *token]() { |
| 224 |
2/3✓ Branch 3 → 4 taken 3 times.
✓ Branch 5 → 6 taken 3 times.
✗ Branch 5 → 8 not taken.
|
3 | if (const auto record = m_cache->journal().mutation(token)) |
| 225 |
1/1✓ Branch 7 → 8 taken 3 times.
|
3 | dispatchMutation(*record); |
| 226 | 3 | }); | |
| 227 | 8 | return *token; | |
| 228 | 13 | } | |
| 229 | |||
| 230 | ✗ | void MailMutationPipeline::queueAppendAsync(const QString &folderPath, | |
| 231 | const QByteArray &rfcMessage, | ||
| 232 | const QString &flags, | ||
| 233 | const QString &messageId, | ||
| 234 | const QByteArray &precomputedSha256, | ||
| 235 | std::function<void(qint64)> callback) { | ||
| 236 | ✗ | const QString canonicalMessageId = MessageId::canonicalFirst(messageId); | |
| 237 | ✗ | if (m_folder.accountId.isEmpty() || folderPath.isEmpty() || | |
| 238 | ✗ | canonicalMessageId.isEmpty() || rfcMessage.isEmpty() || | |
| 239 | ✗ | rfcMessage.size() > MailCache::MaximumAppendMessageBytes || | |
| 240 | ✗ | precomputedSha256.size() != 32 || | |
| 241 | ✗ | !m_cache->isOpen() || m_cache->databasePath().isEmpty() || | |
| 242 | ✗ | m_pendingAppendWrites >= 2 || | |
| 243 | ✗ | m_pendingAppendWriteBytes > 2 * MailCache::MaximumAppendMessageBytes - | |
| 244 | ✗ | rfcMessage.size()) { | |
| 245 | ✗ | m_deps.statusMessage(tr("Message copy not queued: invalid metadata or busy writer")); | |
| 246 | ✗ | if (callback) callback(0); | |
| 247 | ✗ | return; | |
| 248 | } | ||
| 249 | ✗ | MutationRecord record; | |
| 250 | ✗ | record.mailId = MailId{m_folder.accountId, 0, 0}; | |
| 251 | ✗ | record.folderPath = folderPath; | |
| 252 | ✗ | record.kind = MutationKind::Append; | |
| 253 | ✗ | record.replayClass = MutationReplayClass::AmbiguousOperation; | |
| 254 | ✗ | record.phase = MutationPhase::Queued; | |
| 255 | ✗ | record.targetFolder = folderPath; | |
| 256 | ✗ | record.messageId = canonicalMessageId; | |
| 257 | ✗ | const QString databasePath = m_cache->databasePath(); | |
| 258 | ✗ | const qint64 retainedBytes = rfcMessage.size(); | |
| 259 | ✗ | ++m_pendingAppendWrites; | |
| 260 | ✗ | m_pendingAppendWriteBytes += retainedBytes; | |
| 261 | struct AppendWriteResult { | ||
| 262 | qint64 token = 0; | ||
| 263 | QString error; | ||
| 264 | }; | ||
| 265 | ✗ | auto *watcher = new QFutureWatcher<AppendWriteResult>(this); | |
| 266 | ✗ | connect(watcher, &QFutureWatcher<AppendWriteResult>::finished, this, | |
| 267 | ✗ | [this, watcher, record, databasePath, retainedBytes, | |
| 268 | ✗ | callback = std::move(callback)]() mutable { | |
| 269 | ✗ | const auto result = watcher->result(); | |
| 270 | ✗ | watcher->deleteLater(); | |
| 271 | ✗ | --m_pendingAppendWrites; | |
| 272 | ✗ | m_pendingAppendWriteBytes -= retainedBytes; | |
| 273 | ✗ | if (result.token > 0) { | |
| 274 | // A profile/account handover cannot dispatch the captured identity. | ||
| 275 | // Its durable journal entry remains available when that account returns. | ||
| 276 | ✗ | if (m_cache->databasePath() == databasePath && | |
| 277 | ✗ | m_folder.accountId == record.mailId.accountId) | |
| 278 | ✗ | QTimer::singleShot(0, this, &MailMutationPipeline::replayMutationJournal); | |
| 279 | } else { | ||
| 280 | ✗ | qCWarning(lcController) << "Mutation journal rejected APPEND:" | |
| 281 | ✗ | << result.error; | |
| 282 | ✗ | m_deps.statusMessage(tr("Message copy not queued: %1").arg(result.error)); | |
| 283 | } | ||
| 284 | ✗ | if (callback) callback(result.token); | |
| 285 | ✗ | }); | |
| 286 | ✗ | watcher->setFuture(QtConcurrent::run(&m_appendWritePool, | |
| 287 | ✗ | [databasePath, record, rfcMessage, flags, | |
| 288 | precomputedSha256]() -> AppendWriteResult { | ||
| 289 | ✗ | MailCache writer; | |
| 290 | ✗ | if (!writer.openExistingWriter(databasePath, 2000)) | |
| 291 | ✗ | return {0, writer.lastError()}; | |
| 292 | ✗ | const auto token = writer.journal().enqueueAppendMutation(record, rfcMessage, flags, | |
| 293 | ✗ | precomputedSha256); | |
| 294 | ✗ | return {token.value_or(0), token ? QString{} : writer.lastError()}; | |
| 295 | ✗ | })); | |
| 296 | ✗ | } | |
| 297 | |||
| 298 | // dispatchMutation step: account, suspend, mailbox availability and | ||
| 299 | // UIDVALIDITY guards. False: the record is not dispatched now (a stale or | ||
| 300 | // unavailable mailbox quarantines it). | ||
| 301 | 3258 | bool MailMutationPipeline::admitMutationForDispatch(const MutationRecord &record) { | |
| 302 | 3258 | if (record.mailId.accountId != m_folder.accountId || | |
| 303 |
2/2✓ Branch 4 → 5 taken 3256 times.
✓ Branch 4 → 11 taken 1 time.
|
3257 | record.phase == MutationPhase::Quarantined || |
| 304 |
2/2✓ Branch 5 → 6 taken 3255 times.
✓ Branch 5 → 11 taken 1 time.
|
3256 | record.phase == MutationPhase::Failed || |
| 305 |
4/4✓ Branch 6 → 7 taken 3254 times.
✓ Branch 6 → 11 taken 1 time.
✓ Branch 8 → 9 taken 3252 times.
✓ Branch 8 → 11 taken 2 times.
|
6509 | record.phase == MutationPhase::Confirmed || |
| 306 |
4/4✓ Branch 3 → 4 taken 3257 times.
✓ Branch 3 → 11 taken 1 time.
✓ Branch 13 → 14 taken 18 times.
✓ Branch 13 → 15 taken 3240 times.
|
9769 | m_mutationsScheduledForDispatch.contains(record.token) || |
| 307 |
2/2✓ Branch 10 → 11 taken 12 times.
✓ Branch 10 → 12 taken 3240 times.
|
3252 | !m_mutationsScheduledForDispatch.isEmpty()) |
| 308 | 18 | return false; | |
| 309 |
1/2✗ Branch 17 → 18 not taken.
✓ Branch 17 → 22 taken 3240 times.
|
3240 | if (NetworkLifecycleCoordinator::instance()->isSuspending()) { |
| 310 | ✗ | m_deps.statusMessage(tr("Action queued until the computer resumes")); | |
| 311 | ✗ | return false; | |
| 312 | } | ||
| 313 | |||
| 314 |
3/6✓ Branch 23 → 24 taken 3240 times.
✗ Branch 23 → 26 not taken.
✗ Branch 25 → 26 not taken.
✓ Branch 25 → 27 taken 3240 times.
✗ Branch 28 → 29 not taken.
✓ Branch 28 → 39 taken 3240 times.
|
6480 | if (m_unavailableFolders.contains(record.folderPath) || |
| 315 | 3240 | m_unavailableFolders.contains(record.targetFolder)) { | |
| 316 | ✗ | m_cache->journal().updateMutationPhase(record.token, MutationPhase::Quarantined, | |
| 317 | ✗ | QStringLiteral("Mailbox unavailable after LIST; action requires review")); | |
| 318 | ✗ | return false; | |
| 319 | } | ||
| 320 | |||
| 321 | 3240 | const quint32 currentValidity = m_cache->uidValidity(record.mailId.folderId); | |
| 322 |
4/4✓ Branch 40 → 41 taken 3227 times.
✓ Branch 40 → 56 taken 13 times.
✓ Branch 41 → 42 taken 3226 times.
✓ Branch 41 → 56 taken 1 time.
|
3240 | if (record.uidValidity != 0 && currentValidity != 0 && |
| 323 |
2/2✓ Branch 42 → 43 taken 2 times.
✓ Branch 42 → 56 taken 3224 times.
|
3226 | currentValidity != record.uidValidity) { |
| 324 | 2 | m_cache->journal().updateMutationPhase( | |
| 325 |
1/1✓ Branch 48 → 49 taken 2 times.
|
2 | record.token, MutationPhase::Quarantined, |
| 326 | 4 | QStringLiteral("UIDVALIDITY changed; stale UID was not replayed")); | |
| 327 |
2/2✓ Branch 52 → 53 taken 2 times.
✓ Branch 53 → 54 taken 2 times.
|
2 | m_deps.statusMessage(tr("Queued action needs review after mailbox reset")); |
| 328 | 2 | return false; | |
| 329 | } | ||
| 330 | 3238 | return true; | |
| 331 | } | ||
| 332 | |||
| 333 | // Sprint 102 S102.5.1: the connection that sends a UID command has the | ||
| 334 | // record's mailbox selected — and, when both are known, under the | ||
| 335 | // UIDVALIDITY the record was queued with. Checked on the live selection, not | ||
| 336 | // only through the cache, whose update order relative to this dispatch is | ||
| 337 | // not guaranteed. Zero stays the unknown-value compatibility arm. | ||
| 338 | 26 | bool MailMutationPipeline::selectedIdentityMatches(const ImapService *service, | |
| 339 | const MutationRecord &record) { | ||
| 340 | 26 | const quint32 selected = service->selectedUidValidity(); | |
| 341 |
2/2✓ Branch 5 → 6 taken 24 times.
✓ Branch 5 → 10 taken 2 times.
|
76 | return service->selectedFolder() == record.folderPath && |
| 342 |
7/8✓ Branch 6 → 7 taken 19 times.
✓ Branch 6 → 9 taken 5 times.
✓ Branch 7 → 8 taken 4 times.
✓ Branch 7 → 9 taken 15 times.
✓ Branch 8 → 9 taken 3 times.
✓ Branch 8 → 10 taken 1 time.
✓ Branch 11 → 12 taken 26 times.
✗ Branch 11 → 13 not taken.
|
50 | (record.uidValidity == 0 || selected == 0 || selected == record.uidValidity); |
| 343 | } | ||
| 344 | |||
| 345 | // dispatchMutation step (F4.3): reserve, claim and send a batch of | ||
| 346 | // same-source/target/epoch moves as one UID MOVE uid-set command. | ||
| 347 | 2827 | void MailMutationPipeline::dispatchMoveBatch(const QList<MutationRecord> &batch) { | |
| 348 | 5 | auto dispatchBatchWith = [this, batch](ImapService *service) { | |
| 349 | // SELECT on the secondary connection can invalidate captured journal | ||
| 350 | // records. Re-read and claim each record before any UID command goes | ||
| 351 | // to the wire; a record that no longer matches keeps its journal | ||
| 352 | // entry and releases its reservation for the next replay pass. | ||
| 353 | 5 | QList<qint64> uids, tokens; | |
| 354 |
2/2✓ Branch 38 → 4 taken 16 times.
✓ Branch 38 → 39 taken 5 times.
|
21 | for (const auto &entry : batch) { |
| 355 |
1/1✓ Branch 6 → 7 taken 16 times.
|
16 | const auto current = m_cache->journal().mutation(entry.token); |
| 356 |
1/2✓ Branch 11 → 12 taken 16 times.
✗ Branch 11 → 19 not taken.
|
32 | if (!current || current->mailId.accountId != m_folder.accountId || |
| 357 |
3/6✓ Branch 8 → 9 taken 16 times.
✗ Branch 8 → 19 not taken.
✓ Branch 14 → 15 taken 16 times.
✗ Branch 14 → 19 not taken.
✗ Branch 21 → 22 not taken.
✓ Branch 21 → 24 taken 16 times.
|
48 | !selectedIdentityMatches(service, *current) || |
| 358 |
2/3✓ Branch 17 → 18 taken 16 times.
✗ Branch 18 → 19 not taken.
✓ Branch 18 → 20 taken 16 times.
|
16 | !m_cache->journal().claimMutationForDispatch(*current)) { |
| 359 | ✗ | releaseDispatchReservation(entry.token); | |
| 360 | ✗ | continue; | |
| 361 | } | ||
| 362 |
1/1✓ Branch 25 → 26 taken 16 times.
|
16 | tokens.append(current->token); |
| 363 |
1/1✓ Branch 27 → 28 taken 16 times.
|
16 | uids.append(current->mailId.uid); |
| 364 | // S106-189: the batch holds the single-flight slot like a single | ||
| 365 | // record does — also when it went out on the already-selected | ||
| 366 | // connection without reserveAndSelectForDispatch. | ||
| 367 |
1/1✓ Branch 29 → 30 taken 16 times.
|
16 | m_mutationsScheduledForDispatch.insert(current->token); |
| 368 | 16 | } | |
| 369 |
1/2✓ Branch 40 → 41 taken 5 times.
✗ Branch 40 → 43 not taken.
|
5 | if (!tokens.isEmpty()) |
| 370 |
1/1✓ Branch 42 → 43 taken 5 times.
|
5 | service->moveMessageSet(uids, batch.first().targetFolder, tokens); |
| 371 | 2832 | }; | |
| 372 |
2/2✓ Branch 4 → 5 taken 2826 times.
✓ Branch 4 → 35 taken 1 time.
|
2827 | if (m_imapConfig.host.isEmpty()) { |
| 373 | const bool mainOwnsFolder = | ||
| 374 |
2/2✓ Branch 9 → 10 taken 2591 times.
✓ Branch 9 → 12 taken 4 times.
|
5421 | batch.first().folderPath == m_folder.currentFolder && |
| 375 |
1/2✗ Branch 11 → 12 not taken.
✓ Branch 11 → 17 taken 2591 times.
|
5186 | (m_imap->state() == ImapService::State::Selected || |
| 376 |
3/4✓ Branch 7 → 8 taken 2595 times.
✓ Branch 7 → 17 taken 231 times.
✓ Branch 15 → 16 taken 4 times.
✗ Branch 15 → 17 not taken.
|
10838 | m_imap->state() == ImapService::State::Idling) && |
| 377 |
2/2✓ Branch 18 → 19 taken 4 times.
✓ Branch 18 → 20 taken 2822 times.
|
2830 | m_imap->selectedFolder() == batch.first().folderPath; |
| 378 |
2/2✓ Branch 20 → 21 taken 4 times.
✓ Branch 20 → 34 taken 2822 times.
|
2826 | if (mainOwnsFolder) { |
| 379 |
2/2✓ Branch 27 → 23 taken 14 times.
✓ Branch 27 → 28 taken 4 times.
|
18 | for (const auto &entry : batch) |
| 380 |
1/1✓ Branch 24 → 25 taken 14 times.
|
14 | m_mutationsScheduledForDispatch.insert(entry.token); |
| 381 |
2/4✓ Branch 29 → 30 taken 4 times.
✓ Branch 30 → 31 taken 4 times.
✗ Branch 86 → 87 not taken.
✗ Branch 86 → 88 not taken.
|
4 | m_imap->executeAfterIdle([dispatchBatchWith, service = m_imap]() { |
| 382 | 4 | dispatchBatchWith(service); | |
| 383 | 4 | }); | |
| 384 | } | ||
| 385 | 2826 | return; | |
| 386 | } | ||
| 387 |
1/1✓ Branch 35 → 36 taken 1 time.
|
1 | m_deps.ensureBodyConnection(); |
| 388 |
1/2✗ Branch 36 → 37 not taken.
✓ Branch 36 → 38 taken 1 time.
|
1 | if (!m_bodyImap) |
| 389 | ✗ | return; | |
| 390 |
2/4✓ Branch 39 → 40 taken 1 time.
✗ Branch 39 → 45 not taken.
✓ Branch 43 → 44 taken 1 time.
✗ Branch 43 → 45 not taken.
|
2 | if (m_bodyImap->state() == ImapService::State::Selected && |
| 391 |
2/4✓ Branch 46 → 47 taken 1 time.
✗ Branch 46 → 48 not taken.
✓ Branch 48 → 49 taken 1 time.
✗ Branch 48 → 51 not taken.
|
2 | m_bodyImap->selectedFolder() == batch.first().folderPath) { |
| 392 |
1/1✓ Branch 49 → 50 taken 1 time.
|
1 | dispatchBatchWith(m_bodyImap); |
| 393 | 1 | return; | |
| 394 | } | ||
| 395 | ✗ | if (m_bodyImap->state() != ImapService::State::Authenticated && | |
| 396 | ✗ | m_bodyImap->state() != ImapService::State::Selected) | |
| 397 | ✗ | return; | |
| 398 | ✗ | QList<qint64> tokens; | |
| 399 | ✗ | for (const auto &entry : batch) tokens.append(entry.token); | |
| 400 | ✗ | reserveAndSelectForDispatch(m_bodyImap, tokens, batch.first().folderPath, | |
| 401 | ✗ | [dispatchBatchWith, service = m_bodyImap]() { | |
| 402 | ✗ | dispatchBatchWith(service); | |
| 403 | ✗ | }); | |
| 404 | 2827 | } | |
| 405 | |||
| 406 | // dispatchMutation step: read and verify an APPEND payload on the payload | ||
| 407 | // lane; the durable claim happens right before the bytes reach the wire. | ||
| 408 | 3 | void MailMutationPipeline::startAppendPayloadRead(ImapService *service, qint64 token, | |
| 409 | const MutationRecord &record) { | ||
| 410 | // Read and verify the potentially 80-MiB payload off the GUI thread. | ||
| 411 | // The token reservation blocks other mutations, while its durable | ||
| 412 | // phase remains Queued until the bytes are ready for the wire. | ||
| 413 | 3 | const QString databasePath = m_cache->databasePath(); | |
| 414 | 3 | const QString account = record.mailId.accountId; | |
| 415 |
1/1✓ Branch 4 → 5 taken 3 times.
|
3 | const QPointer<ImapService> target(service); |
| 416 | 3 | const quint64 readSequence = ++m_appendPayloadReadSequence; | |
| 417 |
1/1✓ Branch 6 → 7 taken 3 times.
|
3 | m_appendPayloadReads.insert(token, {readSequence, target}); |
| 418 |
3/6✓ Branch 8 → 9 taken 3 times.
✓ Branch 9 → 10 taken 3 times.
✗ Branch 10 → 11 not taken.
✓ Branch 10 → 12 taken 3 times.
✗ Branch 30 → 31 not taken.
✗ Branch 30 → 32 not taken.
|
3 | auto *watcher = new QFutureWatcher<AppendPayloadReadResult>(this); |
| 419 | 3 | connect(watcher, &QFutureWatcher<AppendPayloadReadResult>::finished, | |
| 420 |
1/5✓ Branch 14 → 15 taken 3 times.
✗ Branch 35 → 36 not taken.
✗ Branch 35 → 37 not taken.
✗ Branch 38 → 39 not taken.
✗ Branch 38 → 40 not taken.
|
6 | this, [this, watcher, target, account, token, readSequence] { |
| 421 |
1/1✓ Branch 2 → 3 taken 3 times.
|
3 | auto result = watcher->result(); |
| 422 |
1/1✓ Branch 3 → 4 taken 3 times.
|
3 | watcher->deleteLater(); |
| 423 |
1/2✗ Branch 6 → 7 not taken.
✓ Branch 6 → 8 taken 3 times.
|
3 | if (m_appendPayloadReads.value(token).sequence != readSequence) return; |
| 424 |
1/1✓ Branch 8 → 9 taken 3 times.
|
3 | m_appendPayloadReads.remove(token); |
| 425 |
1/1✓ Branch 10 → 11 taken 3 times.
|
3 | const auto current = m_cache->journal().mutation(token); |
| 426 |
1/2✓ Branch 14 → 15 taken 3 times.
✗ Branch 14 → 20 not taken.
|
6 | if (!current || current->kind != MutationKind::Append || |
| 427 |
3/6✓ Branch 12 → 13 taken 3 times.
✗ Branch 12 → 20 not taken.
✓ Branch 17 → 18 taken 3 times.
✗ Branch 17 → 20 not taken.
✗ Branch 22 → 23 not taken.
✓ Branch 22 → 25 taken 3 times.
|
9 | current->mailId.accountId != account || |
| 428 |
1/2✗ Branch 19 → 20 not taken.
✓ Branch 19 → 21 taken 3 times.
|
3 | current->phase != MutationPhase::Queued) { |
| 429 | ✗ | releaseDispatchReservation(token); | |
| 430 | ✗ | return; | |
| 431 | } | ||
| 432 |
4/8✓ Branch 26 → 27 taken 3 times.
✗ Branch 26 → 35 not taken.
✓ Branch 28 → 29 taken 3 times.
✗ Branch 28 → 35 not taken.
✗ Branch 31 → 32 not taken.
✓ Branch 31 → 36 taken 3 times.
✗ Branch 37 → 38 not taken.
✓ Branch 37 → 40 taken 3 times.
|
6 | if (m_folder.accountId != account || !target || |
| 433 |
0/2✗ Branch 34 → 35 not taken.
✗ Branch 34 → 36 not taken.
|
3 | (target->state() != ImapService::State::Authenticated && |
| 434 | ✗ | target->state() != ImapService::State::Selected)) { | |
| 435 | ✗ | releaseDispatchReservation(token); | |
| 436 | ✗ | return; | |
| 437 | } | ||
| 438 |
1/2✗ Branch 41 → 42 not taken.
✓ Branch 41 → 56 taken 3 times.
|
3 | if (!result.error.isEmpty()) { |
| 439 | ✗ | qCWarning(lcController) << "APPEND payload read deferred:" << result.error; | |
| 440 | ✗ | m_mutationsScheduledForDispatch.remove(token); | |
| 441 | ✗ | QTimer::singleShot(2000, this, &MailMutationPipeline::replayMutationJournal); | |
| 442 | ✗ | return; | |
| 443 | } | ||
| 444 |
2/2✓ Branch 57 → 58 taken 1 time.
✓ Branch 57 → 70 taken 2 times.
|
3 | if (!result.payload) { |
| 445 |
1/1✓ Branch 58 → 59 taken 1 time.
|
1 | m_mutationsScheduledForDispatch.remove(token); |
| 446 |
1/1✓ Branch 62 → 63 taken 1 time.
|
2 | m_cache->journal().updateMutationPhase( |
| 447 | token, MutationPhase::Ambiguous, | ||
| 448 |
1/1✓ Branch 61 → 62 taken 1 time.
|
2 | tr("Message content is not persisted; verifying the target " |
| 449 | "mailbox before any user decision")); | ||
| 450 |
1/2✓ Branch 66 → 67 taken 1 time.
✗ Branch 66 → 68 not taken.
|
1 | if (!m_ambiguousReconciliationQueue.contains(token)) |
| 451 |
1/1✓ Branch 67 → 68 taken 1 time.
|
1 | m_ambiguousReconciliationQueue.enqueue(token); |
| 452 | 1 | QTimer::singleShot(0, this, | |
| 453 |
1/1✓ Branch 68 → 69 taken 1 time.
|
1 | &MailMutationPipeline::reconcileNextAmbiguousMutation); |
| 454 | 1 | return; | |
| 455 | } | ||
| 456 |
2/3✓ Branch 72 → 73 taken 2 times.
✗ Branch 73 → 74 not taken.
✓ Branch 73 → 76 taken 2 times.
|
2 | if (!m_cache->journal().claimMutationForDispatch(*current)) { |
| 457 | ✗ | releaseDispatchReservation(token); | |
| 458 | ✗ | return; | |
| 459 | } | ||
| 460 |
1/1✓ Branch 77 → 78 taken 2 times.
|
2 | const quint64 requestId = target->reserveAppendRequestId(); |
| 461 |
1/1✓ Branch 82 → 83 taken 2 times.
|
4 | target->appendMessage(requestId, current->targetFolder, |
| 462 | 2 | result.payload->message, result.payload->flags, token); | |
| 463 |
4/4✓ Branch 85 → 86 taken 2 times.
✓ Branch 85 → 87 taken 1 time.
✓ Branch 89 → 90 taken 2 times.
✓ Branch 89 → 92 taken 1 time.
|
4 | }); |
| 464 |
1/1✓ Branch 20 → 21 taken 3 times.
|
3 | watcher->setFuture(QtConcurrent::run(&m_appendPayloadPool, |
| 465 |
1/5✓ Branch 19 → 20 taken 3 times.
✗ Branch 47 → 48 not taken.
✗ Branch 47 → 49 not taken.
✗ Branch 50 → 51 not taken.
✗ Branch 50 → 52 not taken.
|
6 | [databasePath, account, token]() -> AppendPayloadReadResult { |
| 466 |
1/1✓ Branch 2 → 3 taken 3 times.
|
3 | MailCache reader; |
| 467 |
2/3✓ Branch 3 → 4 taken 3 times.
✗ Branch 4 → 5 not taken.
✓ Branch 4 → 7 taken 3 times.
|
3 | if (!reader.openReadOnly(databasePath, 2000)) |
| 468 | ✗ | return {{}, reader.lastError()}; | |
| 469 | 3 | AppendPayloadReadResult result; | |
| 470 |
1/1✓ Branch 8 → 9 taken 3 times.
|
3 | result.payload = reader.journal().appendPayload(token, account, &result.error); |
| 471 | 3 | return result; | |
| 472 |
0/3✗ Branch 5 → 6 not taken.
✗ Branch 16 → 17 not taken.
✗ Branch 16 → 18 not taken.
|
3 | })); |
| 473 | 6 | } | |
| 474 | |||
| 475 | 3258 | void MailMutationPipeline::dispatchMutation(const MutationRecord &record) { | |
| 476 |
3/3✓ Branch 2 → 3 taken 3258 times.
✓ Branch 3 → 4 taken 20 times.
✓ Branch 3 → 5 taken 3238 times.
|
3258 | if (!admitMutationForDispatch(record)) |
| 477 | 3257 | return; | |
| 478 | |||
| 479 | // F4.3: same-source/target/epoch journal moves ride one UID MOVE uid-set | ||
| 480 | // command instead of one round trip per mail, so a multi-selection bulk | ||
| 481 | // move finishes in one server exchange. | ||
| 482 |
1/1✓ Branch 5 → 6 taken 3238 times.
|
3238 | const QList<MutationRecord> batch = batchableMoveRecords(record); |
| 483 |
2/2✓ Branch 7 → 8 taken 2827 times.
✓ Branch 7 → 10 taken 411 times.
|
3238 | if (batch.size() > 1) { |
| 484 |
1/1✓ Branch 8 → 9 taken 2827 times.
|
2827 | dispatchMoveBatch(batch); |
| 485 | 2827 | return; | |
| 486 | } | ||
| 487 | |||
| 488 | 425 | auto dispatchWith = [this, token = record.token](ImapService *service) { | |
| 489 | // SELECT on the secondary connection can invalidate the captured journal | ||
| 490 | // record. Re-read after reconciliation before any UID command reaches wire. | ||
| 491 |
1/1✓ Branch 3 → 4 taken 14 times.
|
14 | const auto current = m_cache->journal().mutation(token); |
| 492 |
1/2✓ Branch 8 → 9 taken 13 times.
✗ Branch 8 → 20 not taken.
|
27 | if (!current || current->mailId.accountId != m_folder.accountId || |
| 493 |
2/2✓ Branch 10 → 11 taken 10 times.
✓ Branch 10 → 14 taken 3 times.
|
13 | (current->kind != MutationKind::Append && |
| 494 |
6/6✓ Branch 5 → 6 taken 13 times.
✓ Branch 5 → 20 taken 1 time.
✓ Branch 13 → 14 taken 7 times.
✓ Branch 13 → 20 taken 3 times.
✓ Branch 22 → 23 taken 4 times.
✓ Branch 22 → 25 taken 10 times.
|
34 | !selectedIdentityMatches(service, *current)) || |
| 495 |
2/2✓ Branch 15 → 16 taken 7 times.
✓ Branch 15 → 21 taken 3 times.
|
10 | (current->kind != MutationKind::Append && |
| 496 |
2/3✓ Branch 18 → 19 taken 7 times.
✗ Branch 19 → 20 not taken.
✓ Branch 19 → 21 taken 7 times.
|
7 | !m_cache->journal().claimMutationForDispatch(*current))) { |
| 497 | // F4.3: a record that cannot be claimed right now must not silently | ||
| 498 | // park its journal entry — hand the reservation back and re-drive. | ||
| 499 |
1/1✓ Branch 23 → 24 taken 4 times.
|
4 | releaseDispatchReservation(token); |
| 500 | 4 | return; | |
| 501 | } | ||
| 502 | // Non-APPEND operations are claimed above. APPEND waits until its worker | ||
| 503 | // payload read completes, immediately before handing bytes to the wire. | ||
| 504 | 10 | const auto &record = *current; | |
| 505 |
1/1✓ Branch 26 → 27 taken 10 times.
|
10 | m_mutationsScheduledForDispatch.insert(record.token); |
| 506 |
2/2✓ Branch 27 → 28 taken 3 times.
✓ Branch 27 → 29 taken 7 times.
|
10 | if (record.replayClass == MutationReplayClass::DesiredState) { |
| 507 | 3 | service->storeFlag(record.mailId.uid, record.flag, record.desiredState, | |
| 508 |
1/1✓ Branch 28 → 48 taken 3 times.
|
3 | record.token); |
| 509 |
2/2✓ Branch 29 → 30 taken 6 times.
✓ Branch 29 → 31 taken 1 time.
|
7 | } else if (record.kind == MutationKind::Move || |
| 510 |
2/2✓ Branch 30 → 31 taken 1 time.
✓ Branch 30 → 33 taken 5 times.
|
6 | record.kind == MutationKind::Undo) { |
| 511 | 2 | service->moveMessage(record.mailId.uid, record.targetFolder, | |
| 512 |
1/1✓ Branch 31 → 32 taken 2 times.
|
2 | record.token); |
| 513 |
2/2✓ Branch 33 → 34 taken 1 time.
✓ Branch 33 → 35 taken 4 times.
|
5 | } else if (record.kind == MutationKind::Copy) { |
| 514 | 1 | service->copyMessage(record.mailId.uid, record.targetFolder, | |
| 515 |
1/1✓ Branch 34 → 48 taken 1 time.
|
1 | record.token); |
| 516 |
2/2✓ Branch 35 → 36 taken 3 times.
✓ Branch 35 → 37 taken 1 time.
|
4 | } else if (record.kind == MutationKind::Append) { |
| 517 |
1/1✓ Branch 36 → 48 taken 3 times.
|
3 | startAppendPayloadRead(service, token, record); |
| 518 | } else { | ||
| 519 |
1/1✓ Branch 37 → 38 taken 1 time.
|
1 | m_mutationsScheduledForDispatch.remove(record.token); |
| 520 | 2 | m_cache->journal().updateMutationPhase( | |
| 521 |
1/1✓ Branch 43 → 44 taken 1 time.
|
1 | record.token, MutationPhase::Ambiguous, |
| 522 | 2 | QStringLiteral("Operation requires explicit recovery support")); | |
| 523 | } | ||
| 524 |
2/2✓ Branch 50 → 51 taken 10 times.
✓ Branch 50 → 53 taken 4 times.
|
14 | }; |
| 525 | |||
| 526 | // A configured account always dispatches durable mutations on the | ||
| 527 | // non-idling secondary connection. The main connection can change folders | ||
| 528 | // for navigation while a UID command is serialized; keeping the entire | ||
| 529 | // journal transaction on one secondary connection preserves MailId folder | ||
| 530 | // identity through the actual wire command and its verification readback. | ||
| 531 |
2/2✓ Branch 11 → 12 taken 410 times.
✓ Branch 11 → 42 taken 1 time.
|
411 | if (m_imapConfig.host.isEmpty()) { |
| 532 |
6/6✓ Branch 12 → 13 taken 7 times.
✓ Branch 12 → 18 taken 403 times.
✓ Branch 14 → 15 taken 4 times.
✓ Branch 14 → 17 taken 3 times.
✓ Branch 19 → 20 taken 3 times.
✓ Branch 19 → 22 taken 407 times.
|
421 | if (record.kind == MutationKind::Append && |
| 533 |
1/2✗ Branch 16 → 17 not taken.
✓ Branch 16 → 18 taken 4 times.
|
11 | (m_imap->state() == ImapService::State::Authenticated || |
| 534 | 4 | m_imap->state() == ImapService::State::Selected)) { | |
| 535 |
1/1✓ Branch 20 → 21 taken 3 times.
|
3 | dispatchWith(m_imap); |
| 536 | 3 | return; | |
| 537 | } | ||
| 538 | const bool mainOwnsFolder = | ||
| 539 |
2/2✓ Branch 25 → 26 taken 339 times.
✓ Branch 25 → 28 taken 10 times.
|
349 | record.folderPath == m_folder.currentFolder && |
| 540 |
1/2✗ Branch 27 → 28 not taken.
✓ Branch 27 → 32 taken 339 times.
|
688 | (m_imap->state() == ImapService::State::Selected || |
| 541 |
3/4✓ Branch 23 → 24 taken 349 times.
✓ Branch 23 → 32 taken 58 times.
✓ Branch 30 → 31 taken 10 times.
✗ Branch 30 → 32 not taken.
|
1095 | m_imap->state() == ImapService::State::Idling) && |
| 542 |
2/2✓ Branch 33 → 34 taken 10 times.
✓ Branch 33 → 35 taken 397 times.
|
417 | m_imap->selectedFolder() == record.folderPath; |
| 543 |
2/2✓ Branch 35 → 36 taken 10 times.
✓ Branch 35 → 41 taken 397 times.
|
407 | if (mainOwnsFolder) { |
| 544 |
1/1✓ Branch 36 → 37 taken 10 times.
|
10 | m_mutationsScheduledForDispatch.insert(record.token); |
| 545 |
1/1✓ Branch 38 → 39 taken 10 times.
|
10 | m_imap->executeAfterIdle( |
| 546 |
1/1✓ Branch 37 → 38 taken 10 times.
|
30 | [dispatchWith, service = m_imap]() { dispatchWith(service); }); |
| 547 | } | ||
| 548 | 407 | return; | |
| 549 | } | ||
| 550 |
1/1✓ Branch 42 → 43 taken 1 time.
|
1 | m_deps.ensureBodyConnection(); |
| 551 |
1/2✗ Branch 43 → 44 not taken.
✓ Branch 43 → 45 taken 1 time.
|
1 | if (!m_bodyImap) |
| 552 | ✗ | return; | |
| 553 |
2/6✗ Branch 45 → 46 not taken.
✓ Branch 45 → 51 taken 1 time.
✗ Branch 47 → 48 not taken.
✗ Branch 47 → 50 not taken.
✗ Branch 52 → 53 not taken.
✓ Branch 52 → 55 taken 1 time.
|
1 | if (record.kind == MutationKind::Append && |
| 554 | ✗ | (m_bodyImap->state() == ImapService::State::Authenticated || | |
| 555 | ✗ | m_bodyImap->state() == ImapService::State::Selected)) { | |
| 556 | ✗ | dispatchWith(m_bodyImap); | |
| 557 | ✗ | return; | |
| 558 | } | ||
| 559 |
1/4✗ Branch 56 → 57 not taken.
✓ Branch 56 → 61 taken 1 time.
✗ Branch 59 → 60 not taken.
✗ Branch 59 → 61 not taken.
|
1 | if (m_bodyImap->state() == ImapService::State::Selected && |
| 560 |
2/4✗ Branch 62 → 63 not taken.
✓ Branch 62 → 64 taken 1 time.
✗ Branch 64 → 65 not taken.
✓ Branch 64 → 67 taken 1 time.
|
1 | m_bodyImap->selectedFolder() == record.folderPath) { |
| 561 | ✗ | dispatchWith(m_bodyImap); | |
| 562 | ✗ | return; | |
| 563 | } | ||
| 564 |
2/6✗ Branch 68 → 69 not taken.
✓ Branch 68 → 72 taken 1 time.
✗ Branch 70 → 71 not taken.
✗ Branch 70 → 72 not taken.
✗ Branch 73 → 74 not taken.
✓ Branch 73 → 75 taken 1 time.
|
1 | if (m_bodyImap->state() != ImapService::State::Authenticated && |
| 565 | ✗ | m_bodyImap->state() != ImapService::State::Selected) | |
| 566 | ✗ | return; | |
| 567 | |||
| 568 |
2/2✓ Branch 76 → 77 taken 1 time.
✓ Branch 77 → 78 taken 1 time.
|
1 | reserveAndSelectForDispatch(m_bodyImap, {record.token}, record.folderPath, |
| 569 |
1/1✓ Branch 75 → 76 taken 1 time.
|
1 | [dispatchWith, service = m_bodyImap]() { |
| 570 | 1 | dispatchWith(service); | |
| 571 | 1 | }); | |
| 572 | 3238 | } | |
| 573 | |||
| 574 | 3238 | QList<MutationRecord> MailMutationPipeline::batchableMoveRecords( | |
| 575 | const MutationRecord &record) const { | ||
| 576 |
2/4✓ Branch 5 → 6 taken 3238 times.
✓ Branch 5 → 7 taken 3238 times.
✗ Branch 43 → 44 not taken.
✗ Branch 43 → 45 not taken.
|
6476 | QList<MutationRecord> batch{record}; |
| 577 |
2/2✓ Branch 7 → 8 taken 2887 times.
✓ Branch 7 → 9 taken 351 times.
|
3238 | if (record.kind != MutationKind::Move || |
| 578 |
1/2✗ Branch 8 → 9 not taken.
✓ Branch 8 → 10 taken 2887 times.
|
2887 | record.replayClass != MutationReplayClass::AmbiguousOperation) |
| 579 | 351 | return batch; | |
| 580 |
5/5✓ Branch 11 → 12 taken 2887 times.
✓ Branch 12 → 13 taken 2887 times.
✓ Branch 13 → 14 taken 2887 times.
✓ Branch 38 → 15 taken 271816 times.
✓ Branch 38 → 39 taken 267 times.
|
272083 | for (const auto &other : m_cache->journal().pendingMutations(record.mailId.accountId)) { |
| 581 |
2/2✓ Branch 17 → 18 taken 2620 times.
✓ Branch 17 → 19 taken 269196 times.
|
271816 | if (batch.size() >= MaxBatchedMoveMessages) |
| 582 | 2620 | break; | |
| 583 |
1/2✓ Branch 21 → 22 taken 268655 times.
✗ Branch 21 → 31 not taken.
|
268655 | if (other.token == record.token || |
| 584 | 268655 | m_mutationsScheduledForDispatch.contains(other.token) || | |
| 585 |
2/2✓ Branch 22 → 23 taken 268648 times.
✓ Branch 22 → 31 taken 7 times.
|
268655 | other.phase != MutationPhase::Queued || |
| 586 |
2/2✓ Branch 23 → 24 taken 268621 times.
✓ Branch 23 → 31 taken 27 times.
|
268648 | other.kind != MutationKind::Move || |
| 587 |
1/2✓ Branch 24 → 25 taken 268621 times.
✗ Branch 24 → 31 not taken.
|
268621 | other.replayClass != MutationReplayClass::AmbiguousOperation || |
| 588 |
1/2✓ Branch 25 → 26 taken 268621 times.
✗ Branch 25 → 31 not taken.
|
268621 | other.mailId.folderId != record.mailId.folderId || |
| 589 |
1/2✓ Branch 27 → 28 taken 268621 times.
✗ Branch 27 → 31 not taken.
|
268621 | other.folderPath != record.folderPath || |
| 590 |
6/6✓ Branch 19 → 20 taken 268655 times.
✓ Branch 19 → 31 taken 541 times.
✓ Branch 29 → 30 taken 268612 times.
✓ Branch 29 → 31 taken 9 times.
✓ Branch 33 → 34 taken 584 times.
✓ Branch 33 → 35 taken 268612 times.
|
806463 | other.targetFolder != record.targetFolder || |
| 591 |
1/2✗ Branch 30 → 31 not taken.
✓ Branch 30 → 32 taken 268612 times.
|
268612 | other.uidValidity != record.uidValidity) |
| 592 | 584 | continue; | |
| 593 |
1/1✓ Branch 35 → 36 taken 268612 times.
|
268612 | batch.append(other); |
| 594 | 2887 | } | |
| 595 | 2887 | return batch; | |
| 596 | 3238 | } | |
| 597 | |||
| 598 | 20 | void MailMutationPipeline::releaseDispatchReservation(qint64 token) { | |
| 599 | 20 | m_mutationsScheduledForDispatch.remove(token); | |
| 600 | // The journal is single-flight; freeing a reservation must re-drive the | ||
| 601 | // next pending record or the whole pipeline waits for an unrelated event. | ||
| 602 |
1/1✓ Branch 3 → 4 taken 20 times.
|
20 | QTimer::singleShot(0, this, &MailMutationPipeline::replayMutationJournal); |
| 603 | 20 | } | |
| 604 | |||
| 605 | 2101 | void MailMutationPipeline::cancelPendingAppendPayloadReads(ImapService *service) { | |
| 606 | 2101 | QList<qint64> cancelledTokens; | |
| 607 | 2101 | for (auto it = m_appendPayloadReads.cbegin(); | |
| 608 |
1/2✗ Branch 19 → 3 not taken.
✓ Branch 19 → 20 taken 2101 times.
|
2101 | it != m_appendPayloadReads.cend(); ++it) { |
| 609 | ✗ | if (service && it.value().service != service) continue; | |
| 610 | ✗ | const qint64 token = it.key(); | |
| 611 | // No durable phase change has happened; dropping this reservation lets | ||
| 612 | // replay read the same still-Queued journal entry again. | ||
| 613 | ✗ | m_mutationsScheduledForDispatch.remove(token); | |
| 614 | ✗ | cancelledTokens.append(token); | |
| 615 | } | ||
| 616 |
3/4✓ Branch 20 → 21 taken 2101 times.
✓ Branch 21 → 22 taken 2101 times.
✗ Branch 27 → 23 not taken.
✓ Branch 27 → 28 taken 2101 times.
|
2101 | for (const qint64 token : cancelledTokens) |
| 617 | ✗ | m_appendPayloadReads.remove(token); | |
| 618 | 2101 | } | |
| 619 | |||
| 620 | 1 | void MailMutationPipeline::reserveAndSelectForDispatch( | |
| 621 | ImapService *service, const QList<qint64> &tokens, const QString &folderPath, | ||
| 622 | const std::function<void()> &dispatch) { | ||
| 623 |
2/2✓ Branch 8 → 4 taken 1 time.
✓ Branch 8 → 9 taken 1 time.
|
2 | for (qint64 token : tokens) |
| 624 |
1/1✓ Branch 5 → 6 taken 1 time.
|
1 | m_mutationsScheduledForDispatch.insert(token); |
| 625 | // The connection stays armed until the requested folder actually arrives: | ||
| 626 | // a foreign folderSelected (body fetch, navigation) must not consume it. | ||
| 627 | // The previous single-shot connection leaked the reservation on exactly | ||
| 628 | // that race, wedging the single-flight journal behind a token that never | ||
| 629 | // reached the wire (F4.3: bulk moves that "never finish"). | ||
| 630 |
1/1✓ Branch 9 → 10 taken 1 time.
|
1 | const auto connection = std::make_shared<QMetaObject::Connection>(); |
| 631 | 2 | *connection = connect(service, &ImapService::folderSelected, this, | |
| 632 |
2/6✓ Branch 11 → 12 taken 1 time.
✓ Branch 13 → 14 taken 1 time.
✗ Branch 29 → 30 not taken.
✗ Branch 29 → 31 not taken.
✗ Branch 32 → 33 not taken.
✗ Branch 32 → 34 not taken.
|
2 | [this, service, folderPath, dispatch, |
| 633 | connection](const QString &path, int, quint32, quint64) { | ||
| 634 |
1/2✗ Branch 3 → 4 not taken.
✓ Branch 3 → 5 taken 1 time.
|
1 | if (path != folderPath) |
| 635 | ✗ | return; | |
| 636 | 1 | disconnect(*connection); | |
| 637 | 1 | dispatch(); | |
| 638 | 1 | }); | |
| 639 | // Watchdog: a SELECT that never confirms (request coalescing, a folder | ||
| 640 | // the server rejects silently, a stalled socket) must hand still-queued | ||
| 641 | // reservations back instead of blocking every later mutation. Claimed | ||
| 642 | // records keep their reservation until the wire completion owns them. | ||
| 643 |
1/3✓ Branch 20 → 21 taken 1 time.
✗ Branch 39 → 40 not taken.
✗ Branch 39 → 41 not taken.
|
1 | QTimer::singleShot(m_selectFenceTimeoutMs, this, [this, tokens, connection] { |
| 644 | ✗ | disconnect(*connection); | |
| 645 | ✗ | bool released = false; | |
| 646 | ✗ | for (qint64 token : tokens) { | |
| 647 | ✗ | const auto current = m_cache->journal().mutation(token); | |
| 648 | ✗ | if (!current || current->phase == MutationPhase::Queued) { | |
| 649 | ✗ | m_mutationsScheduledForDispatch.remove(token); | |
| 650 | ✗ | released = true; | |
| 651 | } | ||
| 652 | ✗ | } | |
| 653 | ✗ | if (released) | |
| 654 | ✗ | QTimer::singleShot(0, this, &MailMutationPipeline::replayMutationJournal); | |
| 655 | ✗ | }); | |
| 656 |
1/1✓ Branch 22 → 23 taken 1 time.
|
1 | service->selectFolder(folderPath); |
| 657 | 1 | } | |
| 658 | |||
| 659 | 956 | void MailMutationPipeline::replayMutationJournal() { | |
| 660 |
7/7✓ Branch 3 → 4 taken 948 times.
✓ Branch 3 → 6 taken 8 times.
✓ Branch 4 → 5 taken 948 times.
✓ Branch 5 → 6 taken 28 times.
✓ Branch 5 → 7 taken 920 times.
✓ Branch 8 → 9 taken 36 times.
✓ Branch 8 → 10 taken 920 times.
|
956 | if (m_folder.accountId.isEmpty() || !m_cache->isOpen()) |
| 661 | 36 | return; | |
| 662 |
1/1✓ Branch 11 → 12 taken 920 times.
|
920 | const auto records = m_cache->journal().pendingMutations(m_folder.accountId); |
| 663 |
2/2✓ Branch 76 → 14 taken 2885 times.
✓ Branch 76 → 77 taken 920 times.
|
3805 | for (const MutationRecord &record : records) { |
| 664 | // Reentrant replay must not recover a reservation still owned by this | ||
| 665 | // live transport. Disconnect releases ownership before recovery. | ||
| 666 |
2/2✓ Branch 16 → 17 taken 39 times.
✓ Branch 16 → 18 taken 2846 times.
|
2885 | if (m_mutationsScheduledForDispatch.contains(record.token)) |
| 667 | 39 | continue; | |
| 668 |
4/7✓ Branch 18 → 19 taken 2833 times.
✗ Branch 18 → 21 not taken.
✓ Branch 18 → 46 taken 9 times.
✓ Branch 18 → 48 taken 3 times.
✗ Branch 18 → 67 not taken.
✓ Branch 18 → 70 taken 1 time.
✗ Branch 18 → 74 not taken.
|
2846 | switch (record.phase) { |
| 669 | 2833 | case MutationPhase::Queued: | |
| 670 |
1/1✓ Branch 19 → 20 taken 2833 times.
|
2833 | dispatchMutation(record); |
| 671 | 2833 | break; | |
| 672 | ✗ | case MutationPhase::Dispatched: | |
| 673 | ✗ | if (record.replayClass == MutationReplayClass::DesiredState) { | |
| 674 | ✗ | m_cache->journal().updateMutationPhase(record.token, MutationPhase::Queued); | |
| 675 | ✗ | if (const auto reset = m_cache->journal().mutation(record.token)) | |
| 676 | ✗ | dispatchMutation(*reset); | |
| 677 | ✗ | } else if (!m_ambiguousReconciliationQueue.contains(record.token)) { | |
| 678 | ✗ | m_cache->journal().updateMutationPhase(record.token, MutationPhase::Ambiguous, | |
| 679 | ✗ | tr("Awaiting server verification")); | |
| 680 | ✗ | m_ambiguousReconciliationQueue.enqueue(record.token); | |
| 681 | } | ||
| 682 | ✗ | break; | |
| 683 | 9 | case MutationPhase::AwaitingVerification: | |
| 684 |
1/1✓ Branch 46 → 47 taken 9 times.
|
9 | requestMutationVerification(record); |
| 685 | 9 | break; | |
| 686 | 3 | case MutationPhase::Ambiguous: | |
| 687 |
1/2✗ Branch 48 → 49 not taken.
✓ Branch 48 → 63 taken 3 times.
|
3 | if (record.replayClass == MutationReplayClass::DesiredState) { |
| 688 | ✗ | m_cache->journal().updateMutationPhase(record.token, MutationPhase::Queued); | |
| 689 | ✗ | if (const auto reset = m_cache->journal().mutation(record.token)) | |
| 690 | ✗ | dispatchMutation(*reset); | |
| 691 |
1/2✓ Branch 64 → 65 taken 3 times.
✗ Branch 64 → 66 not taken.
|
3 | } else if (!m_ambiguousReconciliationQueue.contains(record.token)) { |
| 692 |
1/1✓ Branch 65 → 66 taken 3 times.
|
3 | m_ambiguousReconciliationQueue.enqueue(record.token); |
| 693 | } | ||
| 694 | 3 | break; | |
| 695 | ✗ | case MutationPhase::Confirmed: | |
| 696 | ✗ | m_cache->journal().removeMutation(record.token); | |
| 697 | ✗ | break; | |
| 698 | 1 | case MutationPhase::Failed: | |
| 699 | case MutationPhase::Quarantined: | ||
| 700 |
1/1✓ Branch 71 → 72 taken 1 time.
|
1 | m_deps.pendingActionChanged(record.token, {}); |
| 701 | 1 | break; | |
| 702 | } | ||
| 703 | } | ||
| 704 |
1/1✓ Branch 77 → 78 taken 920 times.
|
920 | reconcileNextAmbiguousMutation(); |
| 705 | 920 | } | |
| 706 | |||
| 707 | 26 | void MailMutationPipeline::onMutationDispatched(qint64 token) { | |
| 708 | // Keep this token as the single active journal transaction until its | ||
| 709 | // terminal response and (for desired-state mutations) authoritative flag | ||
| 710 | // readback. Releasing it here would allow another folder SELECT to overtake | ||
| 711 | // a serialized UID command on the shared secondary connection. | ||
| 712 |
1/1✓ Branch 5 → 6 taken 26 times.
|
26 | m_cache->journal().updateMutationPhase(token, MutationPhase::Dispatched); |
| 713 | 26 | MailJD::Performance::MutationPerformanceTracker::instance() | |
| 714 | 26 | .mutationDispatched(token); | |
| 715 | 26 | } | |
| 716 | |||
| 717 | 44 | void MailMutationPipeline::onMutationCompleted( | |
| 718 | qint64 token, int outcomeInt, const QString &error, | ||
| 719 | const QString &uidPlusEvidence) { | ||
| 720 |
1/1✓ Branch 3 → 4 taken 44 times.
|
44 | const auto stored = m_cache->journal().mutation(token); |
| 721 |
2/2✓ Branch 5 → 6 taken 2 times.
✓ Branch 5 → 8 taken 42 times.
|
44 | if (!stored) { |
| 722 | // F4.3: the record is already gone; freeing the single-flight slot must | ||
| 723 | // also re-drive the journal instead of waiting for an unrelated event. | ||
| 724 |
1/1✓ Branch 6 → 7 taken 2 times.
|
2 | releaseDispatchReservation(token); |
| 725 | 2 | return; | |
| 726 | } | ||
| 727 | 42 | const MutationRecord record = *stored; | |
| 728 | 42 | const auto outcome = static_cast<ImapService::MutationOutcome>(outcomeInt); | |
| 729 | auto &performanceTracker = | ||
| 730 |
1/1✓ Branch 10 → 11 taken 42 times.
|
42 | MailJD::Performance::MutationPerformanceTracker::instance(); |
| 731 |
1/1✓ Branch 11 → 12 taken 42 times.
|
42 | performanceTracker.protocolFinished(token); |
| 732 | |||
| 733 |
2/2✓ Branch 12 → 13 taken 26 times.
✓ Branch 12 → 55 taken 16 times.
|
42 | if (outcome == ImapService::MutationOutcome::Confirmed) { |
| 734 |
2/2✓ Branch 13 → 14 taken 7 times.
✓ Branch 13 → 27 taken 19 times.
|
26 | if (record.replayClass == MutationReplayClass::DesiredState) { |
| 735 |
1/1✓ Branch 14 → 15 taken 7 times.
|
7 | performanceTracker.reconciliationStarted(token); |
| 736 |
1/1✓ Branch 17 → 18 taken 7 times.
|
7 | m_cache->journal().updateMutationPhase(token, MutationPhase::AwaitingVerification, |
| 737 | {}, uidPlusEvidence); | ||
| 738 |
2/3✓ Branch 20 → 21 taken 7 times.
✓ Branch 22 → 23 taken 7 times.
✗ Branch 22 → 25 not taken.
|
7 | if (const auto awaiting = m_cache->journal().mutation(token)) |
| 739 |
1/1✓ Branch 24 → 25 taken 7 times.
|
7 | requestMutationVerification(*awaiting); |
| 740 | } else { | ||
| 741 | // A confirmed location-changing operation owns its source identity even | ||
| 742 | // when the visible folder generation changed while the command was in | ||
| 743 | // flight (notably the Message-ID based undo flow). Aggregate | ||
| 744 | // messageMoved signals cannot safely infer that folder and are allowed | ||
| 745 | // to be ignored; the durable record is the authoritative cleanup key. | ||
| 746 |
1/1✓ Branch 27 → 28 taken 19 times.
|
19 | performanceTracker.reconciliationStarted(token); |
| 747 |
2/2✓ Branch 28 → 29 taken 7 times.
✓ Branch 28 → 30 taken 12 times.
|
19 | if (record.kind == MutationKind::Move || |
| 748 |
2/2✓ Branch 29 → 30 taken 2 times.
✓ Branch 29 → 35 taken 5 times.
|
7 | record.kind == MutationKind::Undo) { |
| 749 |
1/1✓ Branch 30 → 31 taken 14 times.
|
14 | retireSourceFlagVerification(record); |
| 750 |
1/1✓ Branch 31 → 32 taken 14 times.
|
14 | m_cache->removeHeader(record.mailId.folderId, record.mailId.uid); |
| 751 |
1/1✓ Branch 32 → 33 taken 14 times.
|
14 | m_model->removeByUid(record.mailId.uid, record.mailId.folderId); |
| 752 |
2/2✓ Branch 33 → 34 taken 5 times.
✓ Branch 33 → 35 taken 9 times.
|
14 | if (m_threadModel) |
| 753 | 5 | m_threadModel->removeByUid(record.mailId.uid, | |
| 754 |
1/1✓ Branch 34 → 35 taken 5 times.
|
5 | record.mailId.folderId); |
| 755 | } | ||
| 756 |
1/1✓ Branch 37 → 38 taken 19 times.
|
19 | m_cache->journal().updateMutationPhase(token, MutationPhase::Confirmed, {}, |
| 757 | uidPlusEvidence); | ||
| 758 |
1/1✓ Branch 40 → 41 taken 19 times.
|
19 | m_cache->journal().removeMutation(token); |
| 759 |
2/2✓ Branch 41 → 42 taken 3 times.
✓ Branch 41 → 44 taken 16 times.
|
19 | if (record.kind == MutationKind::Append) { |
| 760 |
2/2✓ Branch 42 → 43 taken 3 times.
✓ Branch 43 → 44 taken 3 times.
|
3 | m_deps.appendMutationCompleted(token, record.targetFolder, |
| 761 | uidPlusEvidence.toLongLong()); | ||
| 762 | } | ||
| 763 |
2/2✓ Branch 44 → 45 taken 2 times.
✓ Branch 44 → 49 taken 17 times.
|
19 | if (record.kind == MutationKind::Undo) |
| 764 |
2/2✓ Branch 45 → 46 taken 2 times.
✓ Branch 46 → 47 taken 2 times.
|
2 | m_deps.statusMessage(tr("Undo completed")); |
| 765 |
1/1✓ Branch 49 → 50 taken 19 times.
|
19 | m_flagIntents.clearMove(record.mailId); |
| 766 |
1/1✓ Branch 50 → 51 taken 19 times.
|
19 | m_mutationsScheduledForDispatch.remove(token); |
| 767 |
1/1✓ Branch 51 → 52 taken 19 times.
|
19 | performanceTracker.reconciliationFinished(token, true); |
| 768 |
1/1✓ Branch 52 → 53 taken 19 times.
|
19 | QTimer::singleShot(0, this, &MailMutationPipeline::replayMutationJournal); |
| 769 | } | ||
| 770 | 26 | return; | |
| 771 | } | ||
| 772 | |||
| 773 |
3/4✓ Branch 55 → 56 taken 12 times.
✓ Branch 55 → 62 taken 4 times.
✗ Branch 60 → 61 not taken.
✓ Branch 60 → 62 taken 12 times.
|
28 | if (outcome == ImapService::MutationOutcome::Rejected && |
| 774 |
5/6✓ Branch 63 → 64 taken 12 times.
✓ Branch 63 → 65 taken 4 times.
✓ Branch 65 → 66 taken 12 times.
✓ Branch 65 → 67 taken 4 times.
✗ Branch 67 → 68 not taken.
✓ Branch 67 → 77 taken 16 times.
|
28 | error == QStringLiteral("Mailbox changed before dispatch")) { |
| 775 | // Nothing reached the wire: another SELECT overtook the queued UID | ||
| 776 | // command. Replay reselects the record's own mailbox. | ||
| 777 | ✗ | m_cache->journal().updateMutationPhase(token, MutationPhase::Queued); | |
| 778 | ✗ | m_mutationsScheduledForDispatch.remove(token); | |
| 779 | ✗ | QTimer::singleShot(0, this, &MailMutationPipeline::replayMutationJournal); | |
| 780 | ✗ | return; | |
| 781 | } | ||
| 782 | |||
| 783 |
2/2✓ Branch 78 → 79 taken 3 times.
✓ Branch 78 → 80 taken 12 times.
|
15 | if (outcome == ImapService::MutationOutcome::CancelledBeforeDispatch || |
| 784 | 3 | (outcome == ImapService::MutationOutcome::Ambiguous && | |
| 785 |
6/6✓ Branch 77 → 78 taken 15 times.
✓ Branch 77 → 86 taken 1 time.
✓ Branch 79 → 80 taken 2 times.
✓ Branch 79 → 86 taken 1 time.
✓ Branch 80 → 81 taken 12 times.
✓ Branch 80 → 87 taken 2 times.
|
43 | record.replayClass == MutationReplayClass::DesiredState) || |
| 786 |
2/2✓ Branch 85 → 86 taken 1 time.
✓ Branch 85 → 87 taken 11 times.
|
12 | (outcome == ImapService::MutationOutcome::Rejected && |
| 787 |
6/6✓ Branch 88 → 89 taken 12 times.
✓ Branch 88 → 90 taken 4 times.
✓ Branch 90 → 91 taken 12 times.
✓ Branch 90 → 92 taken 4 times.
✓ Branch 92 → 93 taken 3 times.
✓ Branch 92 → 106 taken 13 times.
|
28 | error == QStringLiteral("No folder selected"))) { |
| 788 |
1/1✓ Branch 96 → 97 taken 3 times.
|
6 | m_cache->journal().updateMutationPhase(token, MutationPhase::Queued, |
| 789 |
1/1✓ Branch 95 → 96 taken 3 times.
|
6 | tr("Queued until the mailbox reconnects")); |
| 790 |
2/2✓ Branch 99 → 100 taken 3 times.
✓ Branch 100 → 101 taken 3 times.
|
3 | m_deps.statusMessage(tr("Action queued until the mailbox reconnects")); |
| 791 |
1/1✓ Branch 102 → 103 taken 3 times.
|
3 | m_mutationsScheduledForDispatch.remove(token); |
| 792 |
1/1✓ Branch 103 → 104 taken 3 times.
|
3 | performanceTracker.mutationQueuedOffline(token); |
| 793 | // F4.3: the requeued record needs an explicit re-drive; without it the | ||
| 794 | // single-flight pipeline idles until an unrelated journal event fires. | ||
| 795 |
1/1✓ Branch 104 → 105 taken 3 times.
|
3 | QTimer::singleShot(0, this, &MailMutationPipeline::replayMutationJournal); |
| 796 | 3 | return; | |
| 797 | } | ||
| 798 | |||
| 799 |
2/2✓ Branch 106 → 107 taken 2 times.
✓ Branch 106 → 119 taken 11 times.
|
13 | if (outcome == ImapService::MutationOutcome::Ambiguous) { |
| 800 |
1/1✓ Branch 107 → 108 taken 2 times.
|
2 | performanceTracker.reconciliationStarted(token); |
| 801 |
1/1✓ Branch 110 → 111 taken 2 times.
|
4 | m_cache->journal().updateMutationPhase(token, MutationPhase::Ambiguous, |
| 802 |
1/1✓ Branch 109 → 110 taken 2 times.
|
4 | tr("Awaiting server verification"), |
| 803 | uidPlusEvidence); | ||
| 804 |
1/1✓ Branch 112 → 113 taken 2 times.
|
2 | reportSlowVerification(record); |
| 805 |
1/2✓ Branch 114 → 115 taken 2 times.
✗ Branch 114 → 116 not taken.
|
2 | if (!m_ambiguousReconciliationQueue.contains(token)) |
| 806 |
1/1✓ Branch 115 → 116 taken 2 times.
|
2 | m_ambiguousReconciliationQueue.enqueue(token); |
| 807 |
1/1✓ Branch 116 → 117 taken 2 times.
|
2 | m_mutationsScheduledForDispatch.remove(token); |
| 808 |
1/1✓ Branch 117 → 118 taken 2 times.
|
2 | reconcileNextAmbiguousMutation(); |
| 809 | 2 | return; | |
| 810 | } | ||
| 811 | |||
| 812 |
1/1✓ Branch 120 → 121 taken 11 times.
|
11 | m_cache->journal().updateMutationPhase(token, MutationPhase::Failed, error, |
| 813 | uidPlusEvidence); | ||
| 814 |
1/1✓ Branch 121 → 122 taken 11 times.
|
11 | performanceTracker.reconciliationStarted(token); |
| 815 |
2/2✓ Branch 122 → 123 taken 8 times.
✓ Branch 122 → 124 taken 3 times.
|
11 | if (record.replayClass == MutationReplayClass::DesiredState) { |
| 816 |
1/1✓ Branch 123 → 146 taken 8 times.
|
8 | rollbackDesiredMutation(record); |
| 817 | } else { | ||
| 818 |
2/2✓ Branch 124 → 125 taken 1 time.
✓ Branch 124 → 126 taken 2 times.
|
3 | if (record.kind == MutationKind::Append) { |
| 819 |
1/1✓ Branch 125 → 126 taken 1 time.
|
1 | m_deps.appendMutationFailed(token, error); |
| 820 | } | ||
| 821 |
1/1✓ Branch 126 → 127 taken 3 times.
|
3 | m_flagIntents.clearMove(record.mailId); |
| 822 |
2/2✓ Branch 129 → 130 taken 2 times.
✓ Branch 129 → 144 taken 1 time.
|
3 | if (const auto header = m_cache->header(record.mailId.folderId, |
| 823 |
1/1✓ Branch 127 → 128 taken 3 times.
|
3 | record.mailId.uid)) { |
| 824 |
2/3✓ Branch 130 → 131 taken 2 times.
✓ Branch 131 → 132 taken 2 times.
✗ Branch 131 → 141 not taken.
|
2 | if (m_model->rowForUid(record.mailId.uid, record.mailId.folderId) < 0) |
| 825 |
3/5✓ Branch 135 → 136 taken 2 times.
✓ Branch 138 → 139 taken 2 times.
✓ Branch 138 → 140 taken 2 times.
✗ Branch 207 → 208 not taken.
✗ Branch 207 → 209 not taken.
|
4 | m_model->appendHeaders({*header}); |
| 826 |
1/2✓ Branch 141 → 142 taken 2 times.
✗ Branch 141 → 144 not taken.
|
2 | if (m_threadModel) |
| 827 |
1/1✓ Branch 143 → 144 taken 2 times.
|
2 | m_threadModel->setHeaders(m_model->allHeaders()); |
| 828 | 3 | } | |
| 829 | } | ||
| 830 | // Failed copies retain their exact payload for an explicit APPEND-only retry. | ||
| 831 | // SMTP must never be repeated to recover a rejected Sent copy. | ||
| 832 |
3/3✓ Branch 146 → 147 taken 10 times.
✓ Branch 146 → 149 taken 1 time.
✓ Branch 148 → 149 taken 10 times.
|
11 | if (record.kind != MutationKind::Append) m_cache->journal().removeMutation(token); |
| 833 |
1/1✓ Branch 149 → 150 taken 11 times.
|
11 | m_mutationsScheduledForDispatch.remove(token); |
| 834 |
1/1✓ Branch 150 → 151 taken 11 times.
|
11 | performanceTracker.reconciliationFinished(token, false); |
| 835 |
1/1✓ Branch 151 → 152 taken 11 times.
|
11 | QTimer::singleShot(0, this, &MailMutationPipeline::replayMutationJournal); |
| 836 |
1/1✓ Branch 153 → 154 taken 11 times.
|
11 | m_deps.pendingActionChanged(token, {}); |
| 837 |
3/3✓ Branch 155 → 156 taken 11 times.
✓ Branch 156 → 157 taken 11 times.
✓ Branch 157 → 158 taken 11 times.
|
11 | m_deps.actionError(tr("Action failed in %1: %2").arg(record.folderPath, error)); |
| 838 | 77 | } | |
| 839 | |||
| 840 | 8 | void MailMutationPipeline::rollbackDesiredMutation(const MutationRecord &record) { | |
| 841 |
1/1✓ Branch 2 → 3 taken 8 times.
|
8 | const auto header = m_cache->header(record.mailId.folderId, record.mailId.uid); |
| 842 |
1/2✗ Branch 4 → 5 not taken.
✓ Branch 4 → 6 taken 8 times.
|
8 | if (!header) |
| 843 | ✗ | return; | |
| 844 |
2/2✓ Branch 6 → 7 taken 4 times.
✓ Branch 6 → 8 taken 4 times.
|
8 | if (record.kind == MutationKind::Seen || |
| 845 |
2/2✓ Branch 7 → 8 taken 2 times.
✓ Branch 7 → 21 taken 2 times.
|
4 | record.kind == MutationKind::Flagged) { |
| 846 |
2/2✓ Branch 8 → 9 taken 4 times.
✓ Branch 8 → 10 taken 2 times.
|
6 | const quint32 bit = record.kind == MutationKind::Seen ? MailFlag::Seen |
| 847 | : MailFlag::Flagged; | ||
| 848 |
2/2✓ Branch 11 → 12 taken 4 times.
✓ Branch 11 → 14 taken 2 times.
|
6 | const quint32 flags = record.desiredState ? (header->flags & ~bit) |
| 849 | 2 | : (header->flags | bit); | |
| 850 |
1/1✓ Branch 16 → 17 taken 6 times.
|
6 | m_cache->updateFlags(record.mailId.folderId, record.mailId.uid, flags); |
| 851 |
1/1✓ Branch 17 → 18 taken 6 times.
|
6 | m_model->updateFlags(record.mailId.uid, flags, record.mailId.folderId); |
| 852 |
1/2✓ Branch 18 → 19 taken 6 times.
✗ Branch 18 → 20 not taken.
|
6 | if (m_threadModel) |
| 853 | 6 | m_threadModel->updateFlags(record.mailId.uid, flags, | |
| 854 |
1/1✓ Branch 19 → 20 taken 6 times.
|
6 | record.mailId.folderId); |
| 855 |
1/2✓ Branch 21 → 22 taken 2 times.
✗ Branch 21 → 25 not taken.
|
8 | } else if (record.kind == MutationKind::Keyword) { |
| 856 |
2/2✓ Branch 22 → 23 taken 1 time.
✓ Branch 22 → 24 taken 1 time.
|
2 | if (record.desiredState) |
| 857 | 1 | m_cache->removeLabel(record.mailId.folderId, record.mailId.uid, | |
| 858 |
1/1✓ Branch 23 → 25 taken 1 time.
|
1 | record.flag); |
| 859 | else | ||
| 860 | 1 | m_cache->addLabel(record.mailId.folderId, record.mailId.uid, | |
| 861 |
1/1✓ Branch 24 → 25 taken 1 time.
|
1 | record.flag); |
| 862 | } | ||
| 863 |
1/1✓ Branch 25 → 26 taken 8 times.
|
8 | m_flagIntents.clearFlag(record.mailId); |
| 864 |
2/2✓ Branch 26 → 27 taken 4 times.
✓ Branch 26 → 32 taken 4 times.
|
8 | if (record.kind == MutationKind::Seen) { |
| 865 |
1/1✓ Branch 27 → 28 taken 4 times.
|
4 | const int unread = m_cache->unreadCount(record.mailId.folderId); |
| 866 |
1/1✓ Branch 28 → 29 taken 4 times.
|
4 | m_flagIntents.setLastPolledUnread(record.folderPath, unread); |
| 867 |
1/1✓ Branch 29 → 30 taken 4 times.
|
4 | m_cache->storeBadge(record.mailId.folderId, unread); |
| 868 |
1/1✓ Branch 30 → 31 taken 4 times.
|
4 | m_deps.cachedUnreadCountChanged(record.folderPath, unread); |
| 869 |
1/1✓ Branch 31 → 32 taken 4 times.
|
4 | m_deps.unreadCountChanged(record.folderPath, unread); |
| 870 | } | ||
| 871 | 8 | } | |
| 872 | |||
| 873 | 19 | void MailMutationPipeline::reportSlowVerification(const MutationRecord &record) { | |
| 874 |
2/2✓ Branch 3 → 4 taken 11 times.
✓ Branch 3 → 5 taken 8 times.
|
19 | if (m_slowVerificationReports.contains(record.token)) return; |
| 875 |
1/1✓ Branch 5 → 6 taken 8 times.
|
8 | m_slowVerificationReports.insert(record.token); |
| 876 | // Verification is ordinary background work. Only an unresolved operation | ||
| 877 | // after a readable grace period warrants interrupting the user. | ||
| 878 | 8 | const auto generation = m_mutationVerificationGeneration; | |
| 879 | 8 | const auto database = m_cache->databasePath(); | |
| 880 |
1/3✓ Branch 9 → 10 taken 8 times.
✗ Branch 18 → 19 not taken.
✗ Branch 18 → 20 not taken.
|
8 | QTimer::singleShot(10000, this, [this, record, generation, database] { |
| 881 | 4 | if (generation != m_mutationVerificationGeneration || | |
| 882 |
5/10✓ Branch 2 → 3 taken 1 time.
✗ Branch 2 → 8 not taken.
✓ Branch 5 → 6 taken 1 time.
✗ Branch 5 → 8 not taken.
✗ Branch 7 → 8 not taken.
✓ Branch 7 → 9 taken 1 time.
✓ Branch 10 → 11 taken 1 time.
✗ Branch 10 → 12 not taken.
✗ Branch 12 → 13 not taken.
✓ Branch 12 → 14 taken 1 time.
|
1 | database != m_cache->databasePath() || record.mailId.accountId != m_folder.accountId) return; |
| 883 |
1/1✓ Branch 15 → 16 taken 1 time.
|
1 | const auto current = m_cache->journal().mutation(record.token); |
| 884 |
3/6✓ Branch 17 → 18 taken 1 time.
✗ Branch 17 → 22 not taken.
✗ Branch 19 → 20 not taken.
✓ Branch 19 → 23 taken 1 time.
✗ Branch 24 → 25 not taken.
✓ Branch 24 → 26 taken 1 time.
|
1 | if (!current || (current->phase != MutationPhase::AwaitingVerification && |
| 885 | ✗ | current->phase != MutationPhase::Ambiguous)) return; | |
| 886 | // P90.15: interrupt once per operation, never per folder in a burst. | ||
| 887 | // While several operations are unresolved the report aggregates to a | ||
| 888 | // single count line instead of cycling per-folder texts. | ||
| 889 | 1 | int unresolved = 0; | |
| 890 |
5/5✓ Branch 27 → 28 taken 1 time.
✓ Branch 28 → 29 taken 1 time.
✓ Branch 29 → 30 taken 1 time.
✓ Branch 39 → 31 taken 1 time.
✓ Branch 39 → 40 taken 1 time.
|
2 | for (const auto &pending : m_cache->journal().pendingMutations(m_folder.accountId)) |
| 891 |
1/2✗ Branch 32 → 33 not taken.
✓ Branch 32 → 36 taken 1 time.
|
1 | if (pending.phase == MutationPhase::AwaitingVerification || |
| 892 | ✗ | pending.phase == MutationPhase::Ambiguous || | |
| 893 | ✗ | pending.phase == MutationPhase::Queued || pending.phase == MutationPhase::Dispatched) | |
| 894 | 2 | ++unresolved; | |
| 895 |
1/2✗ Branch 41 → 42 not taken.
✓ Branch 41 → 46 taken 1 time.
|
1 | if (unresolved > 1) { |
| 896 | ✗ | m_deps.pendingActionChanged(record.token, | |
| 897 | ✗ | tr("%n change(s) not yet confirmed by the server", nullptr, unresolved)); | |
| 898 | ✗ | return; | |
| 899 | } | ||
| 900 |
2/2✓ Branch 46 → 47 taken 1 time.
✓ Branch 49 → 50 taken 1 time.
|
2 | m_deps.pendingActionChanged(record.token, tr("Change in %1 not yet confirmed by the server") |
| 901 |
1/1✓ Branch 48 → 49 taken 1 time.
|
2 | .arg(record.folderPath)); |
| 902 |
1/2✓ Branch 54 → 55 taken 1 time.
✗ Branch 54 → 57 not taken.
|
1 | }); |
| 903 | 8 | } | |
| 904 | |||
| 905 | 17 | void MailMutationPipeline::retireSourceFlagVerification(const MutationRecord &record) { | |
| 906 |
1/1✓ Branch 3 → 4 taken 17 times.
|
17 | const auto tokens = m_cache->journal().retireSourceFlagMutations(record.mailId, record.uidValidity); |
| 907 |
2/2✓ Branch 12 → 6 taken 3 times.
✓ Branch 12 → 13 taken 17 times.
|
20 | for (qint64 token : tokens) { |
| 908 |
1/1✓ Branch 7 → 8 taken 3 times.
|
3 | m_mutationsScheduledForDispatch.remove(token); |
| 909 |
1/1✓ Branch 8 → 9 taken 3 times.
|
3 | MailJD::Performance::MutationPerformanceTracker::instance() |
| 910 |
1/1✓ Branch 9 → 10 taken 3 times.
|
3 | .reconciliationSuperseded(token); |
| 911 | } | ||
| 912 |
3/3✓ Branch 14 → 15 taken 3 times.
✓ Branch 14 → 16 taken 14 times.
✓ Branch 15 → 16 taken 3 times.
|
17 | if (!tokens.isEmpty()) m_flagIntents.clearFlag(record.mailId); |
| 913 | 17 | } | |
| 914 | |||
| 915 | 12 | void MailMutationPipeline::deferMutationVerification(const MutationRecord &record) { | |
| 916 | // A failed readback owns no live wire transaction. Retain the unresolved | ||
| 917 | // journal intent, but release the single-flight slot for unrelated actions. | ||
| 918 |
1/1✓ Branch 2 → 3 taken 12 times.
|
12 | m_mutationsScheduledForVerification.remove(record.token); |
| 919 | 12 | const bool alreadyDeferred = m_deferredMutationVerifications.contains(record.token); | |
| 920 |
1/1✓ Branch 4 → 5 taken 12 times.
|
12 | m_deferredMutationVerifications.insert(record.token); |
| 921 |
1/1✓ Branch 5 → 6 taken 12 times.
|
12 | releaseDispatchReservation(record.token); |
| 922 |
6/6✓ Branch 6 → 7 taken 7 times.
✓ Branch 6 → 9 taken 5 times.
✓ Branch 8 → 9 taken 1 time.
✓ Branch 8 → 10 taken 6 times.
✓ Branch 11 → 12 taken 6 times.
✓ Branch 11 → 13 taken 6 times.
|
12 | if (alreadyDeferred || m_mutationVerificationAttempts.value(record.token) >= 3) return; |
| 923 | 6 | const auto generation = m_mutationVerificationGeneration; | |
| 924 | 6 | const auto database = m_cache->databasePath(); | |
| 925 |
1/3✓ Branch 16 → 17 taken 6 times.
✗ Branch 25 → 26 not taken.
✗ Branch 25 → 27 not taken.
|
6 | QTimer::singleShot(1000, this, [this, record, generation, database] { |
| 926 | 8 | if (generation != m_mutationVerificationGeneration || | |
| 927 |
5/10✓ Branch 2 → 3 taken 2 times.
✗ Branch 2 → 8 not taken.
✓ Branch 5 → 6 taken 2 times.
✗ Branch 5 → 8 not taken.
✗ Branch 7 → 8 not taken.
✓ Branch 7 → 9 taken 2 times.
✓ Branch 10 → 11 taken 2 times.
✗ Branch 10 → 12 not taken.
✗ Branch 12 → 13 not taken.
✓ Branch 12 → 14 taken 2 times.
|
2 | database != m_cache->databasePath() || record.mailId.accountId != m_folder.accountId) return; |
| 928 |
1/1✓ Branch 14 → 15 taken 2 times.
|
2 | m_deferredMutationVerifications.remove(record.token); |
| 929 |
1/1✓ Branch 16 → 17 taken 2 times.
|
2 | const auto current = m_cache->journal().mutation(record.token); |
| 930 |
4/8✓ Branch 18 → 19 taken 2 times.
✗ Branch 18 → 25 not taken.
✓ Branch 20 → 21 taken 2 times.
✗ Branch 20 → 25 not taken.
✓ Branch 23 → 24 taken 2 times.
✗ Branch 23 → 25 not taken.
✓ Branch 26 → 27 taken 2 times.
✗ Branch 26 → 29 not taken.
|
4 | if (current && current->phase == MutationPhase::AwaitingVerification && |
| 931 | 2 | current->mailId == record.mailId) | |
| 932 |
1/1✓ Branch 28 → 29 taken 2 times.
|
2 | requestMutationVerification(*current); |
| 933 | 2 | }); | |
| 934 | 6 | } | |
| 935 | |||
| 936 | 18 | void MailMutationPipeline::requestMutationVerification( | |
| 937 | const MutationRecord &record) { | ||
| 938 |
1/2✓ Branch 5 → 6 taken 18 times.
✗ Branch 5 → 10 not taken.
|
36 | if (record.mailId.accountId != m_folder.accountId || |
| 939 |
2/2✓ Branch 7 → 8 taken 12 times.
✓ Branch 7 → 10 taken 6 times.
|
36 | m_mutationsScheduledForVerification.contains(record.token) || |
| 940 |
4/6✓ Branch 3 → 4 taken 18 times.
✗ Branch 3 → 10 not taken.
✗ Branch 9 → 10 not taken.
✓ Branch 9 → 11 taken 12 times.
✓ Branch 12 → 13 taken 6 times.
✓ Branch 12 → 14 taken 12 times.
|
54 | m_deferredMutationVerifications.contains(record.token) || |
| 941 | 12 | m_mutationVerificationAttempts.value(record.token) >= 3) | |
| 942 | 16 | return; | |
| 943 | 12 | const auto generation = m_mutationVerificationGeneration; | |
| 944 | 12 | const auto database = m_cache->databasePath(); | |
| 945 | 5 | auto fetchWith = [this, record, generation, database](ImapService *service) { | |
| 946 |
4/8✓ Branch 2 → 3 taken 5 times.
✗ Branch 2 → 8 not taken.
✓ Branch 5 → 6 taken 5 times.
✗ Branch 5 → 8 not taken.
✓ Branch 10 → 11 taken 5 times.
✗ Branch 10 → 12 not taken.
✗ Branch 12 → 13 not taken.
✓ Branch 12 → 14 taken 5 times.
|
10 | if (generation != m_mutationVerificationGeneration || database != m_cache->databasePath() || |
| 947 |
1/2✗ Branch 7 → 8 not taken.
✓ Branch 7 → 9 taken 5 times.
|
5 | record.mailId.accountId != m_folder.accountId) return; |
| 948 |
1/1✓ Branch 15 → 16 taken 5 times.
|
5 | const auto current = m_cache->journal().mutation(record.token); |
| 949 |
1/2✓ Branch 19 → 20 taken 5 times.
✗ Branch 19 → 25 not taken.
|
10 | if (!current || current->phase != MutationPhase::AwaitingVerification || |
| 950 |
4/8✓ Branch 17 → 18 taken 5 times.
✗ Branch 17 → 25 not taken.
✓ Branch 22 → 23 taken 5 times.
✗ Branch 22 → 25 not taken.
✗ Branch 24 → 25 not taken.
✓ Branch 24 → 26 taken 5 times.
✗ Branch 27 → 28 not taken.
✓ Branch 27 → 29 taken 5 times.
|
10 | current->mailId != record.mailId || current->uidValidity != record.uidValidity) |
| 951 | ✗ | return; | |
| 952 |
1/1✓ Branch 29 → 30 taken 5 times.
|
5 | m_mutationsScheduledForVerification.insert(record.token); |
| 953 |
1/1✓ Branch 30 → 31 taken 5 times.
|
5 | ++m_mutationVerificationAttempts[record.token]; |
| 954 |
1/1✓ Branch 31 → 32 taken 5 times.
|
5 | reportSlowVerification(record); |
| 955 | // F-38: read back only the changed UID. The previous whole-folder | ||
| 956 | // UID FETCH 1:* returned one full flag snapshot (838 kB on a | ||
| 957 | // 20,000-message folder) per flag change, which multiplied into | ||
| 958 | // minutes for mark-folder-read and selection actions. | ||
| 959 |
1/1✓ Branch 32 → 33 taken 5 times.
|
5 | service->fetchVerificationFlags(record.mailId.uid); |
| 960 |
1/2✓ Branch 35 → 36 taken 5 times.
✗ Branch 35 → 38 not taken.
|
17 | }; |
| 961 |
2/2✓ Branch 18 → 19 taken 10 times.
✓ Branch 18 → 38 taken 2 times.
|
12 | if (m_imapConfig.host.isEmpty()) { |
| 962 |
1/2✗ Branch 22 → 23 not taken.
✓ Branch 22 → 27 taken 6 times.
|
6 | if ((m_imap->state() == ImapService::State::Selected || |
| 963 |
3/4✓ Branch 20 → 21 taken 6 times.
✓ Branch 20 → 23 taken 4 times.
✓ Branch 25 → 26 taken 4 times.
✗ Branch 25 → 27 not taken.
|
20 | m_imap->state() == ImapService::State::Idling) && |
| 964 |
4/4✓ Branch 28 → 29 taken 4 times.
✓ Branch 28 → 30 taken 6 times.
✓ Branch 30 → 31 taken 4 times.
✓ Branch 30 → 37 taken 6 times.
|
14 | m_imap->selectedFolder() == record.folderPath) { |
| 965 |
1/1✓ Branch 33 → 34 taken 4 times.
|
4 | m_imap->executeAfterIdle( |
| 966 |
1/3✓ Branch 32 → 33 taken 4 times.
✗ Branch 96 → 97 not taken.
✗ Branch 96 → 98 not taken.
|
12 | [fetchWith, service = m_imap]() { fetchWith(service); }); |
| 967 | } | ||
| 968 | 10 | return; | |
| 969 | } | ||
| 970 |
1/1✓ Branch 38 → 39 taken 2 times.
|
2 | m_deps.ensureBodyConnection(); |
| 971 |
1/2✗ Branch 39 → 40 not taken.
✓ Branch 39 → 41 taken 2 times.
|
2 | if (!m_bodyImap) |
| 972 | ✗ | return; | |
| 973 |
1/4✗ Branch 42 → 43 not taken.
✓ Branch 42 → 47 taken 2 times.
✗ Branch 45 → 46 not taken.
✗ Branch 45 → 47 not taken.
|
2 | if (m_bodyImap->state() == ImapService::State::Selected && |
| 974 |
2/4✗ Branch 48 → 49 not taken.
✓ Branch 48 → 50 taken 2 times.
✗ Branch 50 → 51 not taken.
✓ Branch 50 → 53 taken 2 times.
|
2 | m_bodyImap->selectedFolder() == record.folderPath) { |
| 975 | ✗ | fetchWith(m_bodyImap); | |
| 976 | ✗ | return; | |
| 977 | } | ||
| 978 |
2/6✗ Branch 54 → 55 not taken.
✓ Branch 54 → 58 taken 2 times.
✗ Branch 56 → 57 not taken.
✗ Branch 56 → 58 not taken.
✗ Branch 59 → 60 not taken.
✓ Branch 59 → 61 taken 2 times.
|
2 | if (m_bodyImap->state() != ImapService::State::Authenticated && |
| 979 | ✗ | m_bodyImap->state() != ImapService::State::Selected) | |
| 980 | ✗ | return; | |
| 981 |
1/1✓ Branch 61 → 62 taken 2 times.
|
2 | m_mutationsScheduledForVerification.insert(record.token); |
| 982 | // S106-186: the readback fence follows reserveAndSelectForDispatch. A | ||
| 983 | // single-shot connection was consumed by a foreign folderSelected (body | ||
| 984 | // fetch, navigation) and never fired for a rejected SELECT; the token then | ||
| 985 | // kept the verification slot and the single-flight dispatch reservation, | ||
| 986 | // and no other journal record was dispatched until the connection dropped. | ||
| 987 |
1/1✓ Branch 62 → 63 taken 2 times.
|
2 | const auto connection = std::make_shared<QMetaObject::Connection>(); |
| 988 |
1/1✓ Branch 63 → 64 taken 2 times.
|
2 | const auto fenced = std::make_shared<bool>(false); |
| 989 | 4 | *connection = connect(m_bodyImap, &ImapService::folderSelected, this, | |
| 990 |
1/7✓ Branch 68 → 69 taken 2 times.
✗ Branch 105 → 106 not taken.
✗ Branch 105 → 107 not taken.
✗ Branch 108 → 109 not taken.
✗ Branch 108 → 110 not taken.
✗ Branch 111 → 112 not taken.
✗ Branch 111 → 113 not taken.
|
4 | [this, record, fetchWith, connection, |
| 991 | fenced](const QString &path, int, quint32, quint64) { | ||
| 992 |
2/2✓ Branch 3 → 4 taken 1 time.
✓ Branch 3 → 5 taken 1 time.
|
2 | if (path != record.folderPath) |
| 993 | 1 | return; | |
| 994 | 1 | *fenced = true; | |
| 995 | 1 | disconnect(*connection); | |
| 996 | 1 | fetchWith(m_bodyImap); | |
| 997 | 2 | }); | |
| 998 | // Watchdog: a SELECT that never confirms hands the slot back. The attempt | ||
| 999 | // counts against the readback bound so a folder the server keeps rejecting | ||
| 1000 | // stops being retried after three rounds (the record stays pending). | ||
| 1001 |
1/5✓ Branch 76 → 77 taken 2 times.
✗ Branch 118 → 119 not taken.
✗ Branch 118 → 120 not taken.
✗ Branch 121 → 122 not taken.
✗ Branch 121 → 123 not taken.
|
2 | QTimer::singleShot(m_selectFenceTimeoutMs, this, [this, record, connection, fenced] { |
| 1002 |
2/2✓ Branch 3 → 4 taken 1 time.
✓ Branch 3 → 5 taken 1 time.
|
2 | if (*fenced) |
| 1003 | 1 | return; | |
| 1004 | 1 | disconnect(*connection); | |
| 1005 |
1/2✗ Branch 8 → 9 not taken.
✓ Branch 8 → 10 taken 1 time.
|
1 | if (!m_mutationsScheduledForVerification.contains(record.token)) |
| 1006 | ✗ | return; | |
| 1007 |
5/5✓ Branch 10 → 11 taken 1 time.
✓ Branch 14 → 15 taken 1 time.
✓ Branch 15 → 16 taken 1 time.
✓ Branch 23 → 12 taken 1 time.
✓ Branch 23 → 24 taken 1 time.
|
2 | qCWarning(lcController) << "verification SELECT fence timed out" |
| 1008 |
4/4✓ Branch 16 → 17 taken 1 time.
✓ Branch 17 → 18 taken 1 time.
✓ Branch 18 → 19 taken 1 time.
✓ Branch 19 → 20 taken 1 time.
|
1 | << "token" << record.token << "folder" << record.folderPath; |
| 1009 | 1 | ++m_mutationVerificationAttempts[record.token]; | |
| 1010 | 1 | deferMutationVerification(record); | |
| 1011 | }); | ||
| 1012 |
1/1✓ Branch 78 → 79 taken 2 times.
|
2 | m_bodyImap->selectFolder(record.folderPath); |
| 1013 | 22 | } | |
| 1014 | |||
| 1015 | 23 | void MailMutationPipeline::onFlagSnapshotsReceived( | |
| 1016 | const QList<ImapFlagSnapshot> &snapshots, const QString &folderPath) { | ||
| 1017 | 23 | applyFlagSnapshots(qobject_cast<ImapService *>(sender()), snapshots, folderPath, 0); | |
| 1018 | 23 | } | |
| 1019 | |||
| 1020 | // S106-195: a verification readback asked for one UID — its answer (or the | ||
| 1021 | // absence of one) is evidence about that UID's records only, never about | ||
| 1022 | // other records of the folder awaiting their own readback. | ||
| 1023 | 6 | void MailMutationPipeline::onVerificationFlagsReceived( | |
| 1024 | const QList<ImapFlagSnapshot> &snapshots, const QString &folderPath, qint64 requestedUid) { | ||
| 1025 | 6 | applyFlagSnapshots(qobject_cast<ImapService *>(sender()), snapshots, folderPath, | |
| 1026 | requestedUid); | ||
| 1027 | 6 | } | |
| 1028 | |||
| 1029 | 29 | void MailMutationPipeline::applyFlagSnapshots(ImapService *origin, | |
| 1030 | const QList<ImapFlagSnapshot> &snapshots, const QString &folderPath, qint64 onlyUid) { | ||
| 1031 | // Foreground folder sync may have captured flags before the secondary | ||
| 1032 | // connection's STORE. It cannot confirm or reject that transaction. | ||
| 1033 |
2/2✓ Branch 3 → 4 taken 25 times.
✓ Branch 3 → 5 taken 4 times.
|
29 | auto *owner = m_imapConfig.host.isEmpty() ? m_imap : m_bodyImap; |
| 1034 |
4/4✓ Branch 6 → 7 taken 8 times.
✓ Branch 6 → 9 taken 21 times.
✓ Branch 7 → 8 taken 1 time.
✓ Branch 7 → 9 taken 7 times.
|
29 | if (origin && origin != owner) return; |
| 1035 |
1/1✓ Branch 10 → 11 taken 28 times.
|
28 | const auto records = m_cache->journal().pendingMutations(m_folder.accountId); |
| 1036 | 28 | QHash<MailId, qint64> latestDesiredToken; | |
| 1037 |
2/2✓ Branch 24 → 13 taken 49 times.
✓ Branch 24 → 25 taken 28 times.
|
77 | for (const auto &pending : records) { |
| 1038 |
2/2✓ Branch 14 → 15 taken 43 times.
✓ Branch 14 → 22 taken 6 times.
|
49 | if (pending.replayClass == MutationReplayClass::DesiredState && |
| 1039 |
2/2✓ Branch 15 → 16 taken 42 times.
✓ Branch 15 → 22 taken 1 time.
|
43 | pending.phase != MutationPhase::Confirmed && |
| 1040 |
2/2✓ Branch 16 → 17 taken 41 times.
✓ Branch 16 → 22 taken 1 time.
|
42 | pending.phase != MutationPhase::Failed && |
| 1041 |
2/2✓ Branch 17 → 18 taken 40 times.
✓ Branch 17 → 22 taken 1 time.
|
41 | pending.phase != MutationPhase::Quarantined) |
| 1042 | 80 | latestDesiredToken[pending.mailId] = | |
| 1043 |
1/1✓ Branch 20 → 21 taken 40 times.
|
40 | qMax(latestDesiredToken.value(pending.mailId), pending.token); |
| 1044 | } | ||
| 1045 |
2/2✓ Branch 214 → 27 taken 49 times.
✓ Branch 214 → 215 taken 28 times.
|
77 | for (const MutationRecord &record : records) { |
| 1046 | 149 | if (record.phase != MutationPhase::AwaitingVerification || | |
| 1047 |
8/8✓ Branch 28 → 29 taken 36 times.
✓ Branch 28 → 33 taken 13 times.
✓ Branch 30 → 31 taken 35 times.
✓ Branch 30 → 33 taken 1 time.
✓ Branch 31 → 32 taken 7 times.
✓ Branch 31 → 34 taken 28 times.
✓ Branch 35 → 36 taken 15 times.
✓ Branch 35 → 37 taken 34 times.
|
56 | record.folderPath != folderPath || |
| 1048 |
2/2✓ Branch 32 → 33 taken 1 time.
✓ Branch 32 → 34 taken 6 times.
|
7 | (onlyUid != 0 && record.mailId.uid != onlyUid)) |
| 1049 | 27 | continue; | |
| 1050 |
1/1✓ Branch 39 → 40 taken 34 times.
|
34 | const auto snapshotIt = std::find_if( |
| 1051 | 30 | snapshots.cbegin(), snapshots.cend(), [&record](const auto &snapshot) { | |
| 1052 | 30 | return snapshot.uid == record.mailId.uid; | |
| 1053 | }); | ||
| 1054 |
2/2✓ Branch 42 → 43 taken 12 times.
✓ Branch 42 → 68 taken 22 times.
|
34 | if (snapshotIt == snapshots.cend()) { |
| 1055 | // Recover journals left behind by older versions: after a confirmed | ||
| 1056 | // MOVE the header is gone, but its earlier flag readback used to stay | ||
| 1057 | // pending forever. A complete owning-transport snapshot and matching | ||
| 1058 | // UID epoch now also establish that the removed source UID is absent. | ||
| 1059 | // Missing snapshot evidence alone never cancels a still-cached message. | ||
| 1060 |
2/4✓ Branch 44 → 45 taken 5 times.
✗ Branch 44 → 52 not taken.
✓ Branch 45 → 46 taken 5 times.
✗ Branch 45 → 52 not taken.
|
5 | if (origin == owner && origin && record.uidValidity != 0 && |
| 1061 |
4/5✓ Branch 43 → 44 taken 5 times.
✓ Branch 43 → 52 taken 7 times.
✓ Branch 46 → 47 taken 5 times.
✓ Branch 47 → 48 taken 5 times.
✗ Branch 47 → 52 not taken.
|
22 | m_cache->uidValidity(record.mailId.folderId) == record.uidValidity && |
| 1062 |
7/9✓ Branch 48 → 49 taken 5 times.
✓ Branch 50 → 51 taken 1 time.
✓ Branch 50 → 52 taken 4 times.
✓ Branch 53 → 54 taken 5 times.
✓ Branch 53 → 55 taken 7 times.
✓ Branch 55 → 56 taken 1 time.
✓ Branch 55 → 59 taken 11 times.
✗ Branch 222 → 223 not taken.
✗ Branch 222 → 224 not taken.
|
17 | !m_cache->header(record.mailId.folderId, record.mailId.uid)) { |
| 1063 |
1/1✓ Branch 56 → 57 taken 1 time.
|
1 | retireSourceFlagVerification(record); |
| 1064 |
1/1✓ Branch 57 → 58 taken 1 time.
|
1 | QTimer::singleShot(0, this, &MailMutationPipeline::replayMutationJournal); |
| 1065 | 1 | continue; | |
| 1066 | } | ||
| 1067 | 11 | m_cache->journal().updateMutationPhase( | |
| 1068 |
1/1✓ Branch 62 → 63 taken 11 times.
|
11 | record.token, MutationPhase::AwaitingVerification, |
| 1069 |
1/1✓ Branch 61 → 62 taken 11 times.
|
22 | tr("Server did not return this message for verification")); |
| 1070 |
1/1✓ Branch 65 → 66 taken 11 times.
|
11 | reportSlowVerification(record); |
| 1071 |
1/1✓ Branch 66 → 67 taken 11 times.
|
11 | deferMutationVerification(record); |
| 1072 | 11 | continue; | |
| 1073 | } | ||
| 1074 | |||
| 1075 | 22 | bool actual = false; | |
| 1076 |
2/2✓ Branch 68 → 69 taken 14 times.
✓ Branch 68 → 71 taken 8 times.
|
22 | if (record.kind == MutationKind::Seen) |
| 1077 | 14 | actual = snapshotIt->flags & MailFlag::Seen; | |
| 1078 |
2/2✓ Branch 71 → 72 taken 4 times.
✓ Branch 71 → 74 taken 4 times.
|
8 | else if (record.kind == MutationKind::Flagged) |
| 1079 | 4 | actual = snapshotIt->flags & MailFlag::Flagged; | |
| 1080 | else | ||
| 1081 |
1/1✓ Branch 78 → 79 taken 4 times.
|
4 | actual = std::any_of(snapshotIt->keywords.cbegin(), |
| 1082 | 4 | snapshotIt->keywords.cend(), | |
| 1083 | 3 | [&record](const QString &keyword) { | |
| 1084 | 3 | return keyword.compare(record.flag, | |
| 1085 | 3 | Qt::CaseInsensitive) == 0; | |
| 1086 | }); | ||
| 1087 | |||
| 1088 | 22 | const bool matches = actual == record.desiredState; | |
| 1089 | // A confirmed STORE whose readback differs was overtaken by another | ||
| 1090 | // client or, before queued UID commands were bound to their mailbox, sent | ||
| 1091 | // to another mailbox. Send the desired state once more before accepting | ||
| 1092 | // the server state; the optimistic local state stays until then. | ||
| 1093 |
4/4✓ Branch 80 → 81 taken 3 times.
✓ Branch 80 → 84 taken 19 times.
✓ Branch 82 → 83 taken 2 times.
✓ Branch 82 → 84 taken 1 time.
|
22 | const bool resend = !matches && !m_resentMutations.contains(record.token); |
| 1094 | |||
| 1095 | // A readback confirms the command that preceded it. A later queued | ||
| 1096 | // command already owns the optimistic cache/model state for this message. | ||
| 1097 | // Retain that state and its ordinary flag-sync fence until its own readback. | ||
| 1098 |
6/6✓ Branch 85 → 86 taken 20 times.
✓ Branch 85 → 89 taken 2 times.
✓ Branch 87 → 88 taken 17 times.
✓ Branch 87 → 89 taken 3 times.
✓ Branch 90 → 91 taken 17 times.
✓ Branch 90 → 141 taken 5 times.
|
22 | if (!resend && latestDesiredToken.value(record.mailId) <= record.token) { |
| 1099 | 34 | m_cache->updateFlags(record.mailId.folderId, record.mailId.uid, | |
| 1100 |
1/1✓ Branch 92 → 93 taken 17 times.
|
17 | snapshotIt->flags); |
| 1101 |
1/1✓ Branch 94 → 95 taken 17 times.
|
17 | m_model->updateFlags(record.mailId.uid, snapshotIt->flags, |
| 1102 | 17 | record.mailId.folderId); | |
| 1103 |
2/2✓ Branch 95 → 96 taken 16 times.
✓ Branch 95 → 98 taken 1 time.
|
17 | if (m_threadModel) |
| 1104 |
1/1✓ Branch 97 → 98 taken 16 times.
|
16 | m_threadModel->updateFlags(record.mailId.uid, snapshotIt->flags, |
| 1105 | 16 | record.mailId.folderId); | |
| 1106 |
2/2✓ Branch 98 → 99 taken 3 times.
✓ Branch 98 → 140 taken 14 times.
|
17 | if (record.kind == MutationKind::Keyword) { |
| 1107 | const auto cached = | ||
| 1108 |
1/1✓ Branch 99 → 100 taken 3 times.
|
3 | m_cache->header(record.mailId.folderId, record.mailId.uid); |
| 1109 |
1/2✓ Branch 101 → 102 taken 3 times.
✗ Branch 101 → 111 not taken.
|
3 | if (cached) { |
| 1110 |
2/2✓ Branch 109 → 105 taken 4 times.
✓ Branch 109 → 110 taken 3 times.
|
7 | for (const QString &oldLabel : cached->labels) |
| 1111 |
1/1✓ Branch 106 → 107 taken 4 times.
|
4 | m_cache->removeLabel(record.mailId.folderId, record.mailId.uid, |
| 1112 | oldLabel); | ||
| 1113 | } | ||
| 1114 |
2/2✓ Branch 118 → 114 taken 4 times.
✓ Branch 118 → 119 taken 3 times.
|
7 | for (const QString &keyword : snapshotIt->keywords) |
| 1115 |
1/1✓ Branch 115 → 116 taken 4 times.
|
4 | m_cache->addLabel(record.mailId.folderId, record.mailId.uid, keyword); |
| 1116 | 6 | const int row = m_model->rowForUid(record.mailId.uid, | |
| 1117 |
1/1✓ Branch 119 → 120 taken 3 times.
|
3 | record.mailId.folderId); |
| 1118 |
2/2✓ Branch 120 → 121 taken 2 times.
✓ Branch 120 → 135 taken 1 time.
|
3 | if (row >= 0) { |
| 1119 |
2/3✓ Branch 121 → 122 taken 2 times.
✓ Branch 122 → 123 taken 2 times.
✗ Branch 122 → 125 not taken.
|
2 | if (auto *modelHeader = m_model->mutableHeaderAt(row)) |
| 1120 | 2 | modelHeader->labels = snapshotIt->keywords; | |
| 1121 |
1/1✓ Branch 132 → 133 taken 2 times.
|
6 | emit m_model->dataChanged( |
| 1122 |
1/1✓ Branch 131 → 132 taken 2 times.
|
2 | m_model->index(row, 0), |
| 1123 |
2/2✓ Branch 128 → 129 taken 2 times.
✓ Branch 129 → 130 taken 2 times.
|
4 | m_model->index(row, m_model->columnCount() - 1)); |
| 1124 | } | ||
| 1125 |
1/2✓ Branch 135 → 136 taken 3 times.
✗ Branch 135 → 138 not taken.
|
3 | if (m_threadModel) |
| 1126 |
1/1✓ Branch 137 → 138 taken 3 times.
|
3 | m_threadModel->updateLabels(record.mailId.uid, snapshotIt->keywords, |
| 1127 | 3 | record.mailId.folderId); | |
| 1128 | 3 | } | |
| 1129 | |||
| 1130 |
1/1✓ Branch 140 → 141 taken 17 times.
|
17 | m_flagIntents.clearFlag(record.mailId); |
| 1131 | } | ||
| 1132 |
1/1✓ Branch 141 → 142 taken 22 times.
|
22 | m_mutationsScheduledForVerification.remove(record.token); |
| 1133 |
2/2✓ Branch 142 → 143 taken 19 times.
✓ Branch 142 → 157 taken 3 times.
|
22 | if (matches) { |
| 1134 |
1/1✓ Branch 143 → 144 taken 19 times.
|
19 | m_resentMutations.remove(record.token); |
| 1135 |
1/1✓ Branch 144 → 145 taken 19 times.
|
19 | m_deferredMutationVerifications.remove(record.token); |
| 1136 |
1/1✓ Branch 145 → 146 taken 19 times.
|
19 | m_mutationVerificationAttempts.remove(record.token); |
| 1137 |
2/2✓ Branch 146 → 147 taken 13 times.
✓ Branch 146 → 150 taken 6 times.
|
19 | if (record.kind == MutationKind::Seen) { |
| 1138 | 39 | m_flagIntents.setReadbackFence(record.mailId.folderId, { | |
| 1139 |
1/1✓ Branch 147 → 148 taken 13 times.
|
13 | MailJD::Performance::PerformanceTrace::monotonicNowNs(), |
| 1140 |
2/2✓ Branch 148 → 149 taken 13 times.
✓ Branch 149 → 150 taken 13 times.
|
13 | m_cache->uidValidity(record.mailId.folderId)}); |
| 1141 | } | ||
| 1142 |
1/1✓ Branch 151 → 152 taken 19 times.
|
19 | m_cache->journal().removeMutation(record.token); |
| 1143 |
1/1✓ Branch 152 → 153 taken 19 times.
|
19 | m_mutationsScheduledForDispatch.remove(record.token); |
| 1144 |
1/1✓ Branch 153 → 154 taken 19 times.
|
19 | MailJD::Performance::MutationPerformanceTracker::instance() |
| 1145 |
1/1✓ Branch 154 → 155 taken 19 times.
|
19 | .reconciliationFinished(record.token, true); |
| 1146 |
1/1✓ Branch 155 → 156 taken 19 times.
|
19 | QTimer::singleShot(0, this, &MailMutationPipeline::replayMutationJournal); |
| 1147 |
2/2✓ Branch 157 → 158 taken 2 times.
✓ Branch 157 → 186 taken 1 time.
|
3 | } else if (resend) { |
| 1148 |
5/5✓ Branch 158 → 159 taken 2 times.
✓ Branch 162 → 163 taken 2 times.
✓ Branch 163 → 164 taken 2 times.
✓ Branch 173 → 160 taken 2 times.
✓ Branch 173 → 174 taken 2 times.
|
4 | qCInfo(lcController) << "Flag readback differs; sending the change again" |
| 1149 |
3/3✓ Branch 164 → 165 taken 2 times.
✓ Branch 165 → 166 taken 2 times.
✓ Branch 166 → 167 taken 2 times.
|
2 | << "token" << record.token << "folder" |
| 1150 |
3/3✓ Branch 167 → 168 taken 2 times.
✓ Branch 168 → 169 taken 2 times.
✓ Branch 169 → 170 taken 2 times.
|
2 | << record.folderPath << "uid" << record.mailId.uid; |
| 1151 |
1/1✓ Branch 174 → 175 taken 2 times.
|
2 | m_resentMutations.insert(record.token); |
| 1152 |
1/1✓ Branch 175 → 176 taken 2 times.
|
2 | m_deferredMutationVerifications.remove(record.token); |
| 1153 |
1/1✓ Branch 176 → 177 taken 2 times.
|
2 | m_mutationVerificationAttempts.remove(record.token); |
| 1154 |
1/1✓ Branch 180 → 181 taken 2 times.
|
2 | m_cache->journal().updateMutationPhase(record.token, MutationPhase::Queued); |
| 1155 |
1/1✓ Branch 183 → 184 taken 2 times.
|
2 | MailJD::Performance::MutationPerformanceTracker::instance() |
| 1156 |
1/1✓ Branch 184 → 185 taken 2 times.
|
2 | .reconciliationFinished(record.token, false); |
| 1157 |
1/1✓ Branch 185 → 210 taken 2 times.
|
2 | releaseDispatchReservation(record.token); |
| 1158 | } else { | ||
| 1159 | // The server kept another state after a second confirmed STORE. It is | ||
| 1160 | // authoritative (another client or server policy); the snapshot above | ||
| 1161 | // already restored that state locally, so the intent is retired. | ||
| 1162 |
5/5✓ Branch 186 → 187 taken 1 time.
✓ Branch 190 → 191 taken 1 time.
✓ Branch 191 → 192 taken 1 time.
✓ Branch 201 → 188 taken 1 time.
✓ Branch 201 → 202 taken 1 time.
|
2 | qCWarning(lcController) << "Flag readback still differs after resend;" |
| 1163 |
1/1✓ Branch 192 → 193 taken 1 time.
|
1 | << "accepting the server state for token" |
| 1164 |
3/3✓ Branch 193 → 194 taken 1 time.
✓ Branch 194 → 195 taken 1 time.
✓ Branch 195 → 196 taken 1 time.
|
1 | << record.token << "folder" << record.folderPath |
| 1165 |
2/2✓ Branch 196 → 197 taken 1 time.
✓ Branch 197 → 198 taken 1 time.
|
1 | << "uid" << record.mailId.uid; |
| 1166 |
1/1✓ Branch 202 → 203 taken 1 time.
|
1 | m_resentMutations.remove(record.token); |
| 1167 |
1/1✓ Branch 204 → 205 taken 1 time.
|
1 | m_cache->journal().removeMutation(record.token); |
| 1168 |
1/1✓ Branch 205 → 206 taken 1 time.
|
1 | m_mutationsScheduledForDispatch.remove(record.token); |
| 1169 |
1/1✓ Branch 206 → 207 taken 1 time.
|
1 | MailJD::Performance::MutationPerformanceTracker::instance() |
| 1170 |
1/1✓ Branch 207 → 208 taken 1 time.
|
1 | .reconciliationFinished(record.token, false); |
| 1171 |
1/1✓ Branch 208 → 209 taken 1 time.
|
1 | QTimer::singleShot(0, this, &MailMutationPipeline::replayMutationJournal); |
| 1172 | } | ||
| 1173 | } | ||
| 1174 | 28 | } | |
| 1175 | |||
| 1176 | 928 | void MailMutationPipeline::reconcileNextAmbiguousMutation() { | |
| 1177 |
2/2✓ Branch 2 → 3 taken 4 times.
✓ Branch 2 → 4 taken 924 times.
|
928 | if (m_activeReconciliationToken != 0) |
| 1178 | 4 | return; | |
| 1179 |
2/2✓ Branch 29 → 5 taken 8 times.
✓ Branch 29 → 30 taken 920 times.
|
928 | while (!m_ambiguousReconciliationQueue.isEmpty()) { |
| 1180 |
1/1✓ Branch 5 → 6 taken 8 times.
|
8 | const qint64 token = m_ambiguousReconciliationQueue.dequeue(); |
| 1181 |
1/1✓ Branch 7 → 8 taken 8 times.
|
8 | const auto record = m_cache->journal().mutation(token); |
| 1182 |
7/8✓ Branch 9 → 10 taken 7 times.
✓ Branch 9 → 17 taken 1 time.
✓ Branch 12 → 13 taken 7 times.
✗ Branch 12 → 17 not taken.
✓ Branch 14 → 15 taken 4 times.
✓ Branch 14 → 17 taken 3 times.
✓ Branch 19 → 20 taken 4 times.
✓ Branch 19 → 21 taken 4 times.
|
12 | if (!record || record->mailId.accountId != m_folder.accountId || record->phase != MutationPhase::Ambiguous || |
| 1183 |
1/2✗ Branch 16 → 17 not taken.
✓ Branch 16 → 18 taken 4 times.
|
4 | record->replayClass != MutationReplayClass::AmbiguousOperation) |
| 1184 | 4 | continue; | |
| 1185 |
1/1✓ Branch 22 → 23 taken 4 times.
|
4 | reconcileAmbiguousMutation(*record); |
| 1186 | 4 | return; | |
| 1187 | 8 | } | |
| 1188 | } | ||
| 1189 | |||
| 1190 | // reconcileAmbiguousMutation step: a matching Message-ID in the target | ||
| 1191 | // proves the MOVE/COPY/APPEND committed; retire it without repeating it. | ||
| 1192 | 3 | void MailMutationPipeline::confirmReconciliationInTarget(const MutationRecord &record, | |
| 1193 | const QList<qint64> &targetUids) { | ||
| 1194 | // A matching Message-ID in the target proves the MOVE/COPY | ||
| 1195 | // committed. Never issue the location-changing command again. | ||
| 1196 |
2/2✓ Branch 2 → 3 taken 1 time.
✓ Branch 2 → 4 taken 2 times.
|
3 | if (record.kind == MutationKind::Move || |
| 1197 |
1/2✗ Branch 3 → 4 not taken.
✓ Branch 3 → 9 taken 1 time.
|
1 | record.kind == MutationKind::Undo) { |
| 1198 | 2 | retireSourceFlagVerification(record); | |
| 1199 | 2 | m_cache->removeHeader(record.mailId.folderId, | |
| 1200 | 2 | record.mailId.uid); | |
| 1201 | 2 | m_model->removeByUid(record.mailId.uid, | |
| 1202 | 2 | record.mailId.folderId); | |
| 1203 |
1/2✓ Branch 7 → 8 taken 2 times.
✗ Branch 7 → 9 not taken.
|
2 | if (m_threadModel) |
| 1204 | 2 | m_threadModel->removeByUid(record.mailId.uid, | |
| 1205 | 2 | record.mailId.folderId); | |
| 1206 | } | ||
| 1207 | 3 | m_cache->journal().removeMutation(record.token); | |
| 1208 | // S106-194: settled like a wire-confirmed or failed move — the identity | ||
| 1209 | // no longer has a pending move (it used to refuse later moves). | ||
| 1210 | 3 | m_flagIntents.clearMove(record.mailId); | |
| 1211 | 3 | MailJD::Performance::MutationPerformanceTracker::instance() | |
| 1212 | 3 | .reconciliationFinished(record.token, true); | |
| 1213 |
2/2✓ Branch 14 → 15 taken 1 time.
✓ Branch 14 → 21 taken 2 times.
|
3 | if (record.kind == MutationKind::Append) { |
| 1214 | 2 | m_deps.appendMutationCompleted(record.token, | |
| 1215 | 1 | record.targetFolder, | |
| 1216 | 1 | targetUids.first()); | |
| 1217 |
2/2✓ Branch 17 → 18 taken 1 time.
✓ Branch 18 → 19 taken 1 time.
|
1 | m_deps.statusMessage(tr("Message copy verified on the server")); |
| 1218 | } else { | ||
| 1219 |
2/2✓ Branch 21 → 22 taken 2 times.
✓ Branch 22 → 23 taken 2 times.
|
2 | m_deps.statusMessage(tr("Move verified on the server")); |
| 1220 | } | ||
| 1221 | 3 | finishAmbiguousReconciliation(); | |
| 1222 | 3 | } | |
| 1223 | |||
| 1224 | // reconcileAmbiguousMutation step: no server copy of an APPEND exists; | ||
| 1225 | // requeue it only when the persisted payload still verifies. | ||
| 1226 | 2 | void MailMutationPipeline::checkAppendPayloadForReconciliation(const MutationRecord &record) { | |
| 1227 | // The bounded target search proves no server copy exists. | ||
| 1228 | // Verify the full persisted BLOB on the same serial read | ||
| 1229 | // lane used by dispatch; return only a verdict to the GUI. | ||
| 1230 | 2 | const QString databasePath = m_cache->databasePath(); | |
| 1231 | 2 | const quint64 sequence = m_reconciliationSequence; | |
| 1232 |
3/6✓ Branch 3 → 4 taken 2 times.
✓ Branch 4 → 5 taken 2 times.
✗ Branch 5 → 6 not taken.
✓ Branch 5 → 7 taken 2 times.
✗ Branch 20 → 21 not taken.
✗ Branch 20 → 22 not taken.
|
2 | auto *watcher = new QFutureWatcher<AppendPayloadCheckResult>(this); |
| 1233 | 2 | connect(watcher, &QFutureWatcher<AppendPayloadCheckResult>::finished, | |
| 1234 |
1/5✓ Branch 9 → 10 taken 2 times.
✗ Branch 25 → 26 not taken.
✗ Branch 25 → 27 not taken.
✗ Branch 28 → 29 not taken.
✗ Branch 28 → 30 not taken.
|
4 | this, [this, watcher, record, databasePath, sequence] { |
| 1235 |
1/1✓ Branch 2 → 3 taken 1 time.
|
1 | const auto result = watcher->result(); |
| 1236 |
1/1✓ Branch 3 → 4 taken 1 time.
|
1 | watcher->deleteLater(); |
| 1237 | 3 | if (sequence != m_reconciliationSequence || | |
| 1238 |
1/2✓ Branch 5 → 6 taken 1 time.
✗ Branch 5 → 11 not taken.
|
1 | m_activeReconciliationToken != record.token || |
| 1239 |
2/4✓ Branch 4 → 5 taken 1 time.
✗ Branch 4 → 11 not taken.
✓ Branch 7 → 8 taken 1 time.
✗ Branch 7 → 11 not taken.
|
3 | m_folder.accountId != record.mailId.accountId || |
| 1240 |
3/6✗ Branch 10 → 11 not taken.
✓ Branch 10 → 12 taken 1 time.
✓ Branch 13 → 14 taken 1 time.
✗ Branch 13 → 15 not taken.
✗ Branch 15 → 16 not taken.
✓ Branch 15 → 17 taken 1 time.
|
2 | m_cache->databasePath() != databasePath) |
| 1241 | ✗ | return; | |
| 1242 |
1/1✓ Branch 18 → 19 taken 1 time.
|
1 | const auto current = m_cache->journal().mutation(record.token); |
| 1243 |
3/6✓ Branch 20 → 21 taken 1 time.
✗ Branch 20 → 25 not taken.
✓ Branch 22 → 23 taken 1 time.
✗ Branch 22 → 25 not taken.
✗ Branch 27 → 28 not taken.
✓ Branch 27 → 30 taken 1 time.
|
2 | if (!current || current->phase != MutationPhase::Ambiguous || |
| 1244 |
1/2✗ Branch 24 → 25 not taken.
✓ Branch 24 → 26 taken 1 time.
|
1 | current->kind != MutationKind::Append) { |
| 1245 | ✗ | finishAmbiguousReconciliation(); | |
| 1246 | ✗ | return; | |
| 1247 | } | ||
| 1248 |
1/2✗ Branch 31 → 32 not taken.
✓ Branch 31 → 46 taken 1 time.
|
1 | if (!result.error.isEmpty()) { |
| 1249 | ✗ | qCWarning(lcController) << "APPEND reconciliation read deferred:" | |
| 1250 | ✗ | << result.error; | |
| 1251 | ✗ | finishAmbiguousReconciliation(); | |
| 1252 | ✗ | QTimer::singleShot(2000, this, | |
| 1253 | ✗ | &MailMutationPipeline::replayMutationJournal); | |
| 1254 | ✗ | return; | |
| 1255 | } | ||
| 1256 |
1/2✓ Branch 46 → 47 taken 1 time.
✗ Branch 46 → 64 not taken.
|
1 | if (result.valid) { |
| 1257 | // The durable journal owns verified bytes; a retry also | ||
| 1258 | // survives process restart. Dispatch rechecks the BLOB | ||
| 1259 | // before handing it to IMAP. | ||
| 1260 |
1/1✓ Branch 50 → 51 taken 1 time.
|
1 | m_cache->journal().updateMutationPhase(record.token, |
| 1261 | MutationPhase::Queued); | ||
| 1262 |
1/1✓ Branch 53 → 54 taken 1 time.
|
1 | MailJD::Performance::MutationPerformanceTracker::instance() |
| 1263 |
1/1✓ Branch 54 → 55 taken 1 time.
|
1 | .reconciliationDeferred(record.token); |
| 1264 |
1/1✓ Branch 55 → 56 taken 1 time.
|
1 | finishAmbiguousReconciliation(); |
| 1265 |
2/3✓ Branch 57 → 58 taken 1 time.
✓ Branch 59 → 60 taken 1 time.
✗ Branch 59 → 62 not taken.
|
1 | if (const auto queued = m_cache->journal().mutation(record.token)) |
| 1266 |
1/1✓ Branch 61 → 62 taken 1 time.
|
1 | dispatchMutation(*queued); |
| 1267 | } else { | ||
| 1268 | ✗ | m_cache->journal().updateMutationPhase( | |
| 1269 | ✗ | record.token, MutationPhase::Ambiguous, | |
| 1270 | ✗ | tr("No matching server copy was found and message " | |
| 1271 | "content is not persisted; manual review required")); | ||
| 1272 | ✗ | m_deps.statusMessage( | |
| 1273 | ✗ | tr("Message copy needs review; it was not repeated")); | |
| 1274 | ✗ | MailJD::Performance::MutationPerformanceTracker::instance() | |
| 1275 | ✗ | .reconciliationFinished(record.token, false); | |
| 1276 | ✗ | finishAmbiguousReconciliation(); | |
| 1277 | } | ||
| 1278 |
2/4✓ Branch 78 → 79 taken 1 time.
✗ Branch 78 → 80 not taken.
✓ Branch 82 → 83 taken 1 time.
✗ Branch 82 → 85 not taken.
|
1 | }); |
| 1279 |
1/1✓ Branch 15 → 16 taken 2 times.
|
2 | watcher->setFuture(QtConcurrent::run(&m_appendPayloadPool, |
| 1280 |
0/4✗ Branch 37 → 38 not taken.
✗ Branch 37 → 39 not taken.
✗ Branch 40 → 41 not taken.
✗ Branch 40 → 42 not taken.
|
4 | [databasePath, account = record.mailId.accountId, |
| 1281 |
1/1✓ Branch 14 → 15 taken 2 times.
|
2 | token = record.token]() -> AppendPayloadCheckResult { |
| 1282 |
1/1✓ Branch 2 → 3 taken 2 times.
|
2 | MailCache reader; |
| 1283 |
2/3✓ Branch 3 → 4 taken 2 times.
✗ Branch 4 → 5 not taken.
✓ Branch 4 → 7 taken 2 times.
|
2 | if (!reader.openReadOnly(databasePath, 2000)) |
| 1284 | ✗ | return {false, reader.lastError()}; | |
| 1285 | 2 | QString readError; | |
| 1286 |
1/1✓ Branch 8 → 9 taken 2 times.
|
4 | const bool valid = reader.journal().appendPayload(token, account, |
| 1287 | 2 | &readError).has_value(); | |
| 1288 | 2 | return {valid, readError}; | |
| 1289 |
0/1✗ Branch 5 → 6 not taken.
|
4 | })); |
| 1290 | 2 | } | |
| 1291 | |||
| 1292 | // reconcileAmbiguousMutation step: a MOVE not found in the target is | ||
| 1293 | // replayed only when the source still holds the exact UID. | ||
| 1294 | 2 | void MailMutationPipeline::searchReconciliationSource(const MutationRecord &record) { | |
| 1295 | auto sourceSelected = connect( | ||
| 1296 | 2 | m_bodyImap, &ImapService::folderSelected, this, | |
| 1297 | 4 | [this, record](const QString &sourcePath, int, quint32, | |
| 1298 | quint64) { | ||
| 1299 |
1/2✗ Branch 3 → 4 not taken.
✓ Branch 3 → 5 taken 2 times.
|
2 | if (sourcePath != record.folderPath) |
| 1300 | ✗ | return; | |
| 1301 |
1/2✓ Branch 6 → 7 taken 2 times.
✗ Branch 6 → 11 not taken.
|
2 | if (!m_reconciliationConnections.isEmpty()) |
| 1302 |
1/1✓ Branch 8 → 9 taken 2 times.
|
2 | QObject::disconnect( |
| 1303 |
1/1✓ Branch 7 → 8 taken 2 times.
|
4 | m_reconciliationConnections.takeFirst()); |
| 1304 |
1/1✓ Branch 13 → 14 taken 2 times.
|
2 | m_reconciliationConnections.append(connect( |
| 1305 |
1/1✓ Branch 12 → 13 taken 2 times.
|
2 | m_bodyImap, &ImapService::searchResultReceived, this, |
| 1306 | 4 | [this, record](const QList<qint64> &sourceUids) { | |
| 1307 |
1/2✓ Branch 3 → 4 taken 2 times.
✗ Branch 3 → 8 not taken.
|
2 | if (!m_reconciliationConnections.isEmpty()) |
| 1308 |
1/1✓ Branch 5 → 6 taken 2 times.
|
2 | QObject::disconnect( |
| 1309 |
1/1✓ Branch 4 → 5 taken 2 times.
|
4 | m_reconciliationConnections.takeFirst()); |
| 1310 |
2/2✓ Branch 9 → 10 taken 1 time.
✓ Branch 9 → 27 taken 1 time.
|
2 | if (sourceUids.contains(record.mailId.uid)) { |
| 1311 | // Source evidence proves the previous dispatch did | ||
| 1312 | // not commit; replaying this exact UID is now safe. | ||
| 1313 | 2 | m_cache->journal().updateMutationPhase( | |
| 1314 |
1/1✓ Branch 13 → 14 taken 1 time.
|
1 | record.token, MutationPhase::Queued); |
| 1315 | 1 | MailJD::Performance::MutationPerformanceTracker::instance() | |
| 1316 | 1 | .reconciliationDeferred(record.token); | |
| 1317 | 1 | finishAmbiguousReconciliation(); | |
| 1318 |
1/2✓ Branch 22 → 23 taken 1 time.
✗ Branch 22 → 25 not taken.
|
1 | if (const auto queued = |
| 1319 |
1/1✓ Branch 20 → 21 taken 1 time.
|
1 | m_cache->journal().mutation(record.token)) |
| 1320 |
1/1✓ Branch 24 → 25 taken 1 time.
|
1 | dispatchMutation(*queued); |
| 1321 | } else { | ||
| 1322 | 1 | m_cache->journal().updateMutationPhase( | |
| 1323 |
1/1✓ Branch 30 → 31 taken 1 time.
|
1 | record.token, MutationPhase::Ambiguous, |
| 1324 |
1/1✓ Branch 29 → 30 taken 1 time.
|
2 | tr("Message was found in neither mailbox; " |
| 1325 | "manual review required")); | ||
| 1326 |
1/1✓ Branch 34 → 35 taken 1 time.
|
1 | m_deps.statusMessage( |
| 1327 |
1/1✓ Branch 33 → 34 taken 1 time.
|
2 | tr("Move needs review; it was not repeated")); |
| 1328 | 1 | MailJD::Performance::MutationPerformanceTracker::instance() | |
| 1329 | 1 | .reconciliationFinished(record.token, false); | |
| 1330 | 1 | finishAmbiguousReconciliation(); | |
| 1331 | } | ||
| 1332 | 2 | })); | |
| 1333 | 2 | m_bodyImap->searchByMessageId(record.messageId); | |
| 1334 |
1/1✓ Branch 3 → 4 taken 2 times.
|
2 | }); |
| 1335 |
1/1✓ Branch 5 → 6 taken 2 times.
|
2 | m_reconciliationConnections.append(sourceSelected); |
| 1336 |
1/1✓ Branch 6 → 7 taken 2 times.
|
2 | m_bodyImap->selectFolder(record.folderPath); |
| 1337 | 2 | } | |
| 1338 | |||
| 1339 | 14 | void MailMutationPipeline::reconcileAmbiguousMutation( | |
| 1340 | const MutationRecord &record) { | ||
| 1341 |
2/2✓ Branch 3 → 4 taken 1 time.
✓ Branch 3 → 5 taken 13 times.
|
20 | if (record.mailId.accountId != m_folder.accountId) return; |
| 1342 |
2/2✓ Branch 6 → 7 taken 1 time.
✓ Branch 6 → 19 taken 12 times.
|
13 | if (record.messageId.isEmpty()) { |
| 1343 | 1 | m_cache->journal().updateMutationPhase( | |
| 1344 |
1/1✓ Branch 10 → 11 taken 1 time.
|
1 | record.token, MutationPhase::Ambiguous, |
| 1345 |
1/1✓ Branch 9 → 10 taken 1 time.
|
2 | tr("No Message-ID is available for safe reconciliation")); |
| 1346 |
2/2✓ Branch 13 → 14 taken 1 time.
✓ Branch 14 → 15 taken 1 time.
|
1 | m_deps.actionError(tr("Move needs review; it was not repeated")); |
| 1347 |
1/1✓ Branch 16 → 17 taken 1 time.
|
1 | MailJD::Performance::MutationPerformanceTracker::instance() |
| 1348 |
1/1✓ Branch 17 → 18 taken 1 time.
|
1 | .reconciliationFinished(record.token, false); |
| 1349 | 1 | return; | |
| 1350 | } | ||
| 1351 | |||
| 1352 |
5/6✓ Branch 19 → 20 taken 5 times.
✓ Branch 19 → 23 taken 7 times.
✓ Branch 21 → 22 taken 5 times.
✗ Branch 21 → 23 not taken.
✓ Branch 24 → 25 taken 5 times.
✓ Branch 24 → 29 taken 7 times.
|
12 | if (!m_bodyImap && m_imapConfig.host.isEmpty()) { |
| 1353 |
2/2✓ Branch 26 → 27 taken 4 times.
✓ Branch 26 → 28 taken 1 time.
|
5 | if (!m_ambiguousReconciliationQueue.contains(record.token)) |
| 1354 |
1/1✓ Branch 27 → 28 taken 4 times.
|
4 | m_ambiguousReconciliationQueue.enqueue(record.token); |
| 1355 | 5 | return; | |
| 1356 | } | ||
| 1357 |
1/1✓ Branch 29 → 30 taken 7 times.
|
7 | m_deps.ensureBodyConnection(); |
| 1358 |
3/6✓ Branch 30 → 31 taken 7 times.
✗ Branch 30 → 35 not taken.
✗ Branch 32 → 33 not taken.
✓ Branch 32 → 36 taken 7 times.
✗ Branch 37 → 38 not taken.
✓ Branch 37 → 42 taken 7 times.
|
14 | if (!m_bodyImap || |
| 1359 |
0/2✗ Branch 34 → 35 not taken.
✗ Branch 34 → 36 not taken.
|
7 | (m_bodyImap->state() != ImapService::State::Authenticated && |
| 1360 | ✗ | m_bodyImap->state() != ImapService::State::Selected)) { | |
| 1361 | ✗ | if (!m_ambiguousReconciliationQueue.contains(record.token)) | |
| 1362 | ✗ | m_ambiguousReconciliationQueue.enqueue(record.token); | |
| 1363 | ✗ | return; | |
| 1364 | } | ||
| 1365 | |||
| 1366 | 7 | m_activeReconciliationToken = record.token; | |
| 1367 | 7 | const quint64 reconciliationSequence = ++m_reconciliationSequence; | |
| 1368 |
1/1✓ Branch 42 → 43 taken 7 times.
|
7 | QTimer::singleShot(30000, this, [this, token = record.token, |
| 1369 | reconciliationSequence]() { | ||
| 1370 | ✗ | if (m_activeReconciliationToken != token || | |
| 1371 | ✗ | m_reconciliationSequence != reconciliationSequence) | |
| 1372 | ✗ | return; | |
| 1373 | ✗ | m_cache->journal().updateMutationPhase( | |
| 1374 | token, MutationPhase::Ambiguous, | ||
| 1375 | ✗ | tr("Server verification timed out; manual review required")); | |
| 1376 | ✗ | const auto record = m_cache->journal().mutation(token); | |
| 1377 | ✗ | m_deps.actionError(record && record->kind == MutationKind::Append | |
| 1378 | ✗ | ? tr("Message copy verification timed out; it was not repeated") | |
| 1379 | : tr("Move verification timed out; it was not repeated")); | ||
| 1380 | ✗ | MailJD::Performance::MutationPerformanceTracker::instance() | |
| 1381 | ✗ | .reconciliationFinished(token, false); | |
| 1382 | ✗ | finishAmbiguousReconciliation(); | |
| 1383 | ✗ | }); | |
| 1384 | auto targetSelected = connect( | ||
| 1385 | 7 | m_bodyImap, &ImapService::folderSelected, this, | |
| 1386 | 14 | [this, record](const QString &path, int, quint32, quint64) { | |
| 1387 |
1/2✗ Branch 3 → 4 not taken.
✓ Branch 3 → 5 taken 6 times.
|
6 | if (path != record.targetFolder) |
| 1388 | ✗ | return; | |
| 1389 |
1/2✓ Branch 6 → 7 taken 6 times.
✗ Branch 6 → 11 not taken.
|
6 | if (!m_reconciliationConnections.isEmpty()) { |
| 1390 |
2/2✓ Branch 7 → 8 taken 6 times.
✓ Branch 8 → 9 taken 6 times.
|
6 | QObject::disconnect(m_reconciliationConnections.takeFirst()); |
| 1391 | } | ||
| 1392 |
1/1✓ Branch 13 → 14 taken 6 times.
|
6 | m_reconciliationConnections.append(connect( |
| 1393 |
1/1✓ Branch 12 → 13 taken 6 times.
|
6 | m_bodyImap, &ImapService::searchResultReceived, this, |
| 1394 | 12 | [this, record](const QList<qint64> &targetUids) { | |
| 1395 |
1/2✓ Branch 3 → 4 taken 6 times.
✗ Branch 3 → 8 not taken.
|
6 | if (!m_reconciliationConnections.isEmpty()) |
| 1396 |
1/1✓ Branch 5 → 6 taken 6 times.
|
6 | QObject::disconnect( |
| 1397 |
1/1✓ Branch 4 → 5 taken 6 times.
|
12 | m_reconciliationConnections.takeFirst()); |
| 1398 |
2/2✓ Branch 9 → 10 taken 2 times.
✓ Branch 9 → 12 taken 4 times.
|
6 | if (!targetUids.isEmpty()) { |
| 1399 | 2 | confirmReconciliationInTarget(record, targetUids); | |
| 1400 | 2 | return; | |
| 1401 | } | ||
| 1402 | |||
| 1403 |
2/2✓ Branch 12 → 13 taken 2 times.
✓ Branch 12 → 15 taken 2 times.
|
4 | if (record.kind == MutationKind::Append) { |
| 1404 | 2 | checkAppendPayloadForReconciliation(record); | |
| 1405 | 2 | return; | |
| 1406 | } | ||
| 1407 | |||
| 1408 | 2 | searchReconciliationSource(record); | |
| 1409 | })); | ||
| 1410 | 6 | m_bodyImap->searchByMessageId(record.messageId); | |
| 1411 |
1/1✓ Branch 44 → 45 taken 7 times.
|
7 | }); |
| 1412 |
1/1✓ Branch 46 → 47 taken 7 times.
|
7 | m_reconciliationConnections.append(targetSelected); |
| 1413 |
1/1✓ Branch 47 → 48 taken 7 times.
|
7 | m_bodyImap->selectFolder(record.targetFolder); |
| 1414 | 7 | } | |
| 1415 | |||
| 1416 | 8 | void MailMutationPipeline::finishAmbiguousReconciliation() { | |
| 1417 | 8 | ++m_reconciliationSequence; | |
| 1418 |
2/2✓ Branch 9 → 5 taken 1 time.
✓ Branch 9 → 10 taken 8 times.
|
9 | for (const auto &connection : std::as_const(m_reconciliationConnections)) |
| 1419 |
1/1✓ Branch 6 → 7 taken 1 time.
|
1 | QObject::disconnect(connection); |
| 1420 | 8 | m_reconciliationConnections.clear(); | |
| 1421 | 8 | m_activeReconciliationToken = 0; | |
| 1422 | 8 | QTimer::singleShot(0, this, | |
| 1423 |
1/1✓ Branch 11 → 12 taken 8 times.
|
8 | &MailMutationPipeline::reconcileNextAmbiguousMutation); |
| 1424 | 8 | } | |
| 1425 |