Akonadi

collectionscheduler.cpp
1 /*
2  Copyright (c) 2014 Daniel Vrátil <[email protected]>
3 
4  This library is free software; you can redistribute it and/or modify it
5  under the terms of the GNU Library General Public License as published by
6  the Free Software Foundation; either version 2 of the License, or (at your
7  option) any later version.
8 
9  This library is distributed in the hope that it will be useful, but WITHOUT
10  ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
11  FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public
12  License for more details.
13 
14  You should have received a copy of the GNU Library General Public License
15  along with this library; see the file COPYING.LIB. If not, write to the
16  Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
17  02110-1301, USA.
18 */
19 
20 #include "collectionscheduler.h"
21 #include "storage/datastore.h"
22 #include "storage/selectquerybuilder.h"
23 #include "akonadiserver_debug.h"
24 
25 #include <private/tristate_p.h>
26 
27 #include <QDateTime>
28 #include <QTimer>
29 
30 using namespace std::literals::chrono_literals;
31 
32 namespace Akonadi
33 {
34 namespace Server
35 {
36 
41 class PauseableTimer : public QTimer
42 {
43  Q_OBJECT
44 
45 public:
46  PauseableTimer(QObject *parent = nullptr)
47  : QTimer(parent)
48  {
49  }
50 
51  void start(int interval)
52  {
53  mStarted = QDateTime::currentDateTimeUtc();
54  mPaused = QDateTime();
55  setInterval(interval);
56  QTimer::start(interval);
57  }
58 
59  void start()
60  {
61  start(interval());
62  }
63 
64  void stop()
65  {
66  mStarted = QDateTime();
67  mPaused = QDateTime();
68  QTimer::stop();
69  }
70 
71  Q_INVOKABLE void pause()
72  {
73  if (!isActive() || isPaused()) {
74  return;
75  }
76 
78  QTimer::stop();
79  }
80 
81  Q_INVOKABLE void resume()
82  {
83  if (!isPaused()) {
84  return;
85  }
86 
87  const int remainder = interval() - (mStarted.secsTo(mPaused) * 1000);
88  start(qMax(0, remainder));
89  mPaused = QDateTime();
90  // Update mStarted so that pause() can be called repeatedly
91  mStarted = QDateTime::currentDateTimeUtc();
92  }
93 
94  bool isPaused() const
95  {
96  return mPaused.isValid();
97  }
98 
99 private:
100  QDateTime mStarted;
101  QDateTime mPaused;
102 };
103 
104 } // namespace Server
105 } // namespace Akonadi
106 
107 using namespace Akonadi::Server;
108 
109 CollectionScheduler::CollectionScheduler(const QString &threadName, QThread::Priority priority, QObject *parent)
110  : AkThread(threadName, priority, parent)
111 {
112 }
113 
114 CollectionScheduler::~CollectionScheduler()
115 {
116 }
117 
118 // Called in secondary thread
119 void CollectionScheduler::quit()
120 {
121  delete mScheduler;
122  mScheduler = nullptr;
123 
124  AkThread::quit();
125 }
126 
127 void CollectionScheduler::inhibit(bool inhibit)
128 {
129  if (inhibit) {
130  const bool success = QMetaObject::invokeMethod(mScheduler, &PauseableTimer::pause, Qt::QueuedConnection);
131  Q_ASSERT(success); Q_UNUSED(success);
132  } else {
133  const bool success = QMetaObject::invokeMethod(mScheduler, &PauseableTimer::resume, Qt::QueuedConnection);
134  Q_ASSERT(success); Q_UNUSED(success);
135  }
136 }
137 
138 int CollectionScheduler::minimumInterval() const
139 {
140  return mMinInterval;
141 }
142 
143 CollectionScheduler::TimePoint CollectionScheduler::nextScheduledTime(qint64 collectionId) const
144 {
145  QMutexLocker locker(&mScheduleLock);
146  const auto i = constFind(collectionId);
147  if (i != mSchedule.cend()) {
148  return i.key();
149  }
150  return {};
151 }
152 
153 std::chrono::milliseconds CollectionScheduler::currentTimerInterval() const
154 {
155  return std::chrono::milliseconds(mScheduler->isActive() ? mScheduler->interval() : 0);
156 }
157 
158 void CollectionScheduler::setMinimumInterval(int intervalMinutes)
159 {
160  // No mutex -- you can only call this before starting the thread
161  mMinInterval = intervalMinutes;
162 }
163 
164 void CollectionScheduler::collectionAdded(qint64 collectionId)
165 {
166  Collection collection = Collection::retrieveById(collectionId);
167  DataStore::self()->activeCachePolicy(collection);
168  if (shouldScheduleCollection(collection)) {
169  QMetaObject::invokeMethod(this, [this, collection]() {scheduleCollection(collection);}, Qt::QueuedConnection);
170  }
171 }
172 
173 void CollectionScheduler::collectionChanged(qint64 collectionId)
174 {
175  QMutexLocker locker(&mScheduleLock);
176  const auto it = constFind(collectionId);
177  if (it != mSchedule.cend()) {
178  const Collection oldCollection = it.value();
179  Collection changed = Collection::retrieveById(collectionId);
180  DataStore::self()->activeCachePolicy(changed);
181  if (hasChanged(oldCollection, changed)) {
182  if (shouldScheduleCollection(changed)) {
183  locker.unlock();
184  // Scheduling the changed collection will automatically remove the old one
185  QMetaObject::invokeMethod(this, [this, changed]() {scheduleCollection(changed);}, Qt::QueuedConnection);
186  } else {
187  locker.unlock();
188  // If the collection should no longer be scheduled then remove it
189  collectionRemoved(collectionId);
190  }
191  }
192  } else {
193  // We don't know the collection yet, but maybe now it can be scheduled
194  collectionAdded(collectionId);
195  }
196 }
197 
198 void CollectionScheduler::collectionRemoved(qint64 collectionId)
199 {
200  QMutexLocker locker(&mScheduleLock);
201  auto it = find(collectionId);
202  if (it != mSchedule.end()) {
203  const bool reschedule = it == mSchedule.begin();
204  mSchedule.erase(it);
205 
206  // If we just remove currently scheduled collection, schedule the next one
207  if (reschedule) {
208  QMetaObject::invokeMethod(this, &CollectionScheduler::startScheduler, Qt::QueuedConnection);
209  }
210  }
211 }
212 
213 // Called in secondary thread
214 void CollectionScheduler::startScheduler()
215 {
216  QMutexLocker locker(&mScheduleLock);
217  // Don't restart timer if we are paused.
218  if (mScheduler->isPaused()) {
219  return;
220  }
221 
222  if (mSchedule.isEmpty()) {
223  // Stop the timer. It will be started again once some collection is scheduled
224  mScheduler->stop();
225  return;
226  }
227 
228  // Get next collection to expire and start the timer
229  const auto next = mSchedule.constBegin().key();
230  // TimePoint uses a signed representation internally (int64_t), so we get negative result when next is in the past
231  const auto delayUntilNext = next - std::chrono::steady_clock::now();
232  mScheduler->start(qMax<qint64>(0, std::chrono::duration_cast<std::chrono::milliseconds>(delayUntilNext).count()));
233 }
234 
235 // Called in secondary thread
236 void CollectionScheduler::scheduleCollection(Collection collection, bool shouldStartScheduler)
237 {
238  DataStore::self()->activeCachePolicy(collection);
239 
240  QMutexLocker locker(&mScheduleLock);
241  auto i = find(collection.id());
242  if (i != mSchedule.end()) {
243  mSchedule.erase(i);
244  }
245 
246  if (!shouldScheduleCollection(collection)) {
247  return;
248  }
249 
250  const int expireMinutes = qMax(mMinInterval, collectionScheduleInterval(collection));
251  TimePoint nextCheck(std::chrono::steady_clock::now() + std::chrono::minutes(expireMinutes));
252 
253  // Check whether there's another check scheduled within a minute after this one.
254  // If yes, then delay this check so that it's scheduled together with the others
255  // This is a minor optimization to reduce wakeups and SQL queries
256  auto it = constLowerBound(nextCheck);
257  if (it != mSchedule.cend() && it.key() - nextCheck < 1min) {
258  nextCheck = it.key();
259 
260  // Also check whether there's another checked scheduled within a minute before
261  // this one.
262  } else if (it != mSchedule.cbegin()) {
263  --it;
264  if (nextCheck - it.key() < 1min) {
265  nextCheck = it.key();
266  }
267  }
268 
269  mSchedule.insert(nextCheck, collection);
270  if (shouldStartScheduler && !mScheduler->isActive()) {
271  locker.unlock();
272  startScheduler();
273  }
274 }
275 
276 CollectionScheduler::ScheduleMap::const_iterator CollectionScheduler::constFind(qint64 collectionId) const
277 {
278  return std::find_if(mSchedule.cbegin(), mSchedule.cend(), [collectionId](const Collection &c) { return c.id() == collectionId; });
279 }
280 
281 CollectionScheduler::ScheduleMap::iterator CollectionScheduler::find(qint64 collectionId)
282 {
283  return std::find_if(mSchedule.begin(), mSchedule.end(), [collectionId](const Collection &c) { return c.id() == collectionId; });
284 }
285 
286 // separate method so we call the const version of QMap::lowerBound
287 CollectionScheduler::ScheduleMap::const_iterator CollectionScheduler::constLowerBound(TimePoint timestamp) const
288 {
289  return mSchedule.lowerBound(timestamp);
290 }
291 
292 // Called in secondary thread
293 void CollectionScheduler::init()
294 {
295  AkThread::init();
296 
297  mScheduler = new PauseableTimer();
298  mScheduler->setSingleShot(true);
299  connect(mScheduler, &QTimer::timeout,
300  this, &CollectionScheduler::schedulerTimeout);
301 
302  // Only retrieve enabled collections and referenced collections, we don't care
303  // about anything else
305  if (!qb.exec()) {
306  qCWarning(AKONADISERVER_LOG) << "Failed to query initial collections for scheduler!";
307  qCWarning(AKONADISERVER_LOG) << "Not a fatal error, no collections will be scheduled for sync or cache expiration!";
308  }
309 
310  const Collection::List collections = qb.result();
311  for (const Collection &collection : collections) {
312  scheduleCollection(collection);
313  }
314 
315  startScheduler();
316 }
317 
318 // Called in secondary thread
319 void CollectionScheduler::schedulerTimeout()
320 {
321  QMutexLocker locker(&mScheduleLock);
322 
323  // Call stop() explicitly to reset the timer
324  mScheduler->stop();
325 
326  const auto timestamp = mSchedule.constBegin().key();
327  const QList<Collection> collections = mSchedule.values(timestamp);
328  mSchedule.remove(timestamp);
329  locker.unlock();
330 
331  for (const Collection &collection : collections) {
332  collectionExpired(collection);
333  scheduleCollection(collection, false);
334  }
335 
336  startScheduler();
337 }
338 
339 #include "collectionscheduler.moc"
Akonadi::Collection collection() const
Returns the currently chosen collection, or an empty collection if none none was chosen.
const QList< QKeySequence > & next()
void timeout()
KGuiItem stop()
void remove(int i)
QVector< T > result()
Returns the result of this SELECT query.
void stop()
bool invokeMethod(QObject *obj, const char *member, Qt::ConnectionType type, QGenericReturnArgument ret, QGenericArgument val0, QGenericArgument val1, QGenericArgument val2, QGenericArgument val3, QGenericArgument val4, QGenericArgument val5, QGenericArgument val6, QGenericArgument val7, QGenericArgument val8, QGenericArgument val9)
Helper class for creating and executing database SELECT queries.
QWidget * find(WId id)
Helper integration between Akonadi and Qt.
QMetaObject::Connection connect(const QObject *sender, const char *signal, const QObject *receiver, const char *method, Qt::ConnectionType type)
QDateTime currentDateTimeUtc()
void start()
bool exec()
Executes the query, returns true on success.
Representation of a record in the Collection table.
Definition: entities.h:451
This file is part of the KDE documentation.
Documentation copyright © 1996-2020 The KDE developers.
Generated on Wed May 27 2020 22:43:37 by doxygen 1.8.11 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.