app/core/DavSyncScheduler.h
| Line | Branch | Exec | Source |
|---|---|---|---|
| 1 | #pragma once | ||
| 2 | |||
| 3 | #include <QHash> | ||
| 4 | #include <QObject> | ||
| 5 | #include <QSet> | ||
| 6 | #include <QStringList> | ||
| 7 | #include <QTimer> | ||
| 8 | #include <functional> | ||
| 9 | |||
| 10 | #include "CalendarAccountSync.h" | ||
| 11 | |||
| 12 | // Sprint 96 S96.3.1/S96.3.4: the single owner of all automatic calendar | ||
| 13 | // read triggers (SPRINT-96.md §4.2/§4.3). Poll timer, push notices, | ||
| 14 | // recovery tickets and manual requests only mark accounts/collections | ||
| 15 | // dirty; the scheduler starts at most one batch per account and never | ||
| 16 | // overlapping loops. | ||
| 17 | // | ||
| 18 | // Invariants: | ||
| 19 | // - A started batch takes over the dirty marks present at its start; marks | ||
| 20 | // arriving while it runs stay dirty and cause exactly one follow-up batch | ||
| 21 | // (catch-up: a push during REPORT/commit is never lost). | ||
| 22 | // - Push notices are coalesced: 500 ms after the latest notice, at most | ||
| 23 | // 2 s after the first; a push-only batch starts at most every 2 s per | ||
| 24 | // account. Manual requests skip debounce and spacing. | ||
| 25 | // - At most two accounts run in parallel; a slow account blocks no other. | ||
| 26 | // - Offline/paused: nothing starts, marks are kept. | ||
| 27 | // - Retry-After and credential/TLS locks hold back automatic batches of | ||
| 28 | // that account; a manual request clears the lock. | ||
| 29 | // - Safety polling with up to 10 % earlier-only jitter keeps running with | ||
| 30 | // healthy push. After a push failure an account polls every 60 s for at | ||
| 31 | // most 15 min (fallback window), then the normal interval applies. | ||
| 32 | class DavSyncScheduler final : public QObject { | ||
| 33 | Q_OBJECT | ||
| 34 | public: | ||
| 35 | struct Timing { | ||
| 36 | int pushDebounceMs = 500; | ||
| 37 | int pushMaxDelayMs = 2000; | ||
| 38 | int minBatchSpacingMs = 2000; | ||
| 39 | int maxParallelAccounts = 2; | ||
| 40 | int fallbackPollMs = 60000; | ||
| 41 | int fallbackWindowMs = 15 * 60000; | ||
| 42 | }; | ||
| 43 | using Executor = std::function<bool(const CalendarSyncRequest &request)>; | ||
| 44 | using Clock = std::function<qint64()>; // monotonic milliseconds | ||
| 45 | |||
| 46 | explicit DavSyncScheduler(Executor executor, QObject *parent = nullptr); | ||
| 47 | |||
| 48 | 41 | void setTiming(const Timing &timing) { m_timing = timing; } | |
| 49 | void setClock(Clock clock) { m_clock = std::move(clock); } | ||
| 50 | 12 | void setJitter(std::function<double()> jitter) { m_jitter = std::move(jitter); } | |
| 51 | 957 | void setNetworkGate(std::function<bool()> gate) { m_networkGate = std::move(gate); } | |
| 52 | // Accounts with selected calendars (polls and "all" requests). | ||
| 53 | 957 | void setAccountsProvider(std::function<QStringList()> accounts) { m_accounts = std::move(accounts); } | |
| 54 | // Called once per poll/manual round before its batches (outbox flush, | ||
| 55 | // ICS subscriptions). | ||
| 56 | 957 | void setRoundHook(std::function<void(unsigned reasons)> hook) { m_roundHook = std::move(hook); } | |
| 57 | |||
| 58 | // Safety poll interval; 0 stops automatic polling. | ||
| 59 | void setPollIntervalMs(qint64 intervalMs); | ||
| 60 | 2 | qint64 nextPollAt() const { return m_nextPollAt; } | |
| 61 | // Milliseconds until the next safety poll; -1 when polling is off. | ||
| 62 |
1/3✗ Branch 2 → 3 not taken.
✓ Branch 2 → 6 taken 2 times.
✗ Branch 3 → 4 not taken.
|
2 | qint64 msUntilNextPoll() const { return m_nextPollAt > 0 ? qMax<qint64>(0, m_nextPollAt - now()) : -1; } |
| 63 | |||
| 64 | // Full run of every account (discovery + CTag), e.g. poll or "Sync now". | ||
| 65 | void requestAll(unsigned reasons); | ||
| 66 | // Full run of one account. | ||
| 67 | void requestAccount(const QString &accountId, unsigned reasons); | ||
| 68 | // Targeted sync of collections (push, recovery); overrides the CTag. | ||
| 69 | void requestCollections(const QString &accountId, const QStringList &paths, unsigned reasons); | ||
| 70 | |||
| 71 | void batchFinished(const CalendarSyncResult &result); | ||
| 72 | void enterFallback(const QString &accountId); | ||
| 73 | void leaveFallback(const QString &accountId); | ||
| 74 | bool inFallback(const QString &accountId) const; | ||
| 75 | void holdUntil(const QString &accountId, qint64 epochMs); // Retry-After | ||
| 76 | void lockAccount(const QString &accountId); // credentials/TLS | ||
| 77 | 2 | bool locked(const QString &accountId) const { return m_locked.contains(accountId); } | |
| 78 | // Re-evaluates after the network gate opened again. | ||
| 79 | void kick(); | ||
| 80 | void stop(); | ||
| 81 | |||
| 82 | 7 | bool busy(const QString &accountId) const { return m_busy.contains(accountId); } | |
| 83 | 4 | bool hasPending(const QString &accountId) const { return m_pending.contains(accountId); } | |
| 84 | int runningCount() const { return int(m_busy.size()); } | ||
| 85 | |||
| 86 | signals: | ||
| 87 | void batchStarted(const CalendarSyncRequest &request); | ||
| 88 | |||
| 89 | private: | ||
| 90 | struct Pending { | ||
| 91 | bool full = false; | ||
| 92 | unsigned reasons = 0; | ||
| 93 | QStringList paths; | ||
| 94 | qint64 dueAt = 0; | ||
| 95 | qint64 firstPushAt = 0; | ||
| 96 | }; | ||
| 97 | void mark(const QString &accountId, bool full, const QStringList &paths, unsigned reasons); | ||
| 98 | void schedule(); | ||
| 99 | void evaluate(); | ||
| 100 | void onPollTimer(); | ||
| 101 | qint64 now() const; | ||
| 102 | qint64 earliestStart(const QString &accountId, const Pending &pending) const; | ||
| 103 | |||
| 104 | Executor m_executor; | ||
| 105 | Clock m_clock; | ||
| 106 | std::function<double()> m_jitter; | ||
| 107 | std::function<bool()> m_networkGate; | ||
| 108 | std::function<QStringList()> m_accounts; | ||
| 109 | std::function<void(unsigned)> m_roundHook; | ||
| 110 | Timing m_timing; | ||
| 111 | QHash<QString, Pending> m_pending; | ||
| 112 | QSet<QString> m_busy; | ||
| 113 | QHash<QString, qint64> m_lastStart; | ||
| 114 | QHash<QString, qint64> m_holdUntil; | ||
| 115 | QHash<QString, qint64> m_fallbackUntil; | ||
| 116 | QHash<QString, qint64> m_nextFallbackPoll; | ||
| 117 | QSet<QString> m_locked; | ||
| 118 | QTimer m_wake; | ||
| 119 | QTimer m_pollTimer; | ||
| 120 | qint64 m_pollIntervalMs = 0; | ||
| 121 | qint64 m_nextPollAt = 0; | ||
| 122 | bool m_stopped = false; | ||
| 123 | }; | ||
| 124 |