1 /*
2 SPDX-FileCopyrightText: 2007 Volker Krause <vkrause@kde.org>
3
4 SPDX-License-Identifier: LGPL-2.0-or-later
5 */
6
7 #include "resourcescheduler_p.h"
8
9 #include "recursivemover_p.h"
10 #include <QDBusConnection>
11
12 #include "akonadiagentbase_debug.h"
13 #include "private/instance_p.h"
14 #include <KLocalizedString>
15
16 #include <QDBusInterface>
17 #include <QTimer>
18
19 using namespace Akonadi;
20 using namespace std::chrono_literals;
21 qint64 ResourceScheduler::Task::latestSerial = 0;
22 static QDBusAbstractInterface *s_resourcetracker = nullptr;
23
24 /// @cond PRIVATE
25
ResourceScheduler(QObject * parent)26 ResourceScheduler::ResourceScheduler(QObject *parent)
27 : QObject(parent)
28 {
29 }
30
scheduleFullSync()31 void ResourceScheduler::scheduleFullSync()
32 {
33 Task t;
34 t.type = SyncAll;
35 TaskList &queue = queueForTaskType(t.type);
36 if (queue.contains(t) || mCurrentTask == t) {
37 return;
38 }
39 queue << t;
40 signalTaskToTracker(t, "SyncAll");
41 scheduleNext();
42 }
43
scheduleCollectionTreeSync()44 void ResourceScheduler::scheduleCollectionTreeSync()
45 {
46 Task t;
47 t.type = SyncCollectionTree;
48 TaskList &queue = queueForTaskType(t.type);
49 if (queue.contains(t) || mCurrentTask == t) {
50 return;
51 }
52 queue << t;
53 signalTaskToTracker(t, "SyncCollectionTree");
54 scheduleNext();
55 }
56
scheduleTagSync()57 void ResourceScheduler::scheduleTagSync()
58 {
59 Task t;
60 t.type = SyncTags;
61 TaskList &queue = queueForTaskType(t.type);
62 if (queue.contains(t) || mCurrentTask == t) {
63 return;
64 }
65 queue << t;
66 signalTaskToTracker(t, "SyncTags");
67 scheduleNext();
68 }
69
scheduleRelationSync()70 void ResourceScheduler::scheduleRelationSync()
71 {
72 Task t;
73 t.type = SyncRelations;
74 TaskList &queue = queueForTaskType(t.type);
75 if (queue.contains(t) || mCurrentTask == t) {
76 return;
77 }
78 queue << t;
79 signalTaskToTracker(t, "SyncRelations");
80 scheduleNext();
81 }
82
scheduleSync(const Collection & col)83 void ResourceScheduler::scheduleSync(const Collection &col)
84 {
85 Task t;
86 t.type = SyncCollection;
87 t.collection = col;
88 TaskList &queue = queueForTaskType(t.type);
89 if (queue.contains(t) || mCurrentTask == t) {
90 return;
91 }
92 queue << t;
93 signalTaskToTracker(t, "SyncCollection", QString::number(col.id()));
94 scheduleNext();
95 }
96
scheduleAttributesSync(const Collection & collection)97 void ResourceScheduler::scheduleAttributesSync(const Collection &collection)
98 {
99 Task t;
100 t.type = SyncCollectionAttributes;
101 t.collection = collection;
102
103 TaskList &queue = queueForTaskType(t.type);
104 if (queue.contains(t) || mCurrentTask == t) {
105 return;
106 }
107 queue << t;
108 signalTaskToTracker(t, "SyncCollectionAttributes", QString::number(collection.id()));
109 scheduleNext();
110 }
111
scheduleItemFetch(const Akonadi::Item & item,const QSet<QByteArray> & parts,const QList<QDBusMessage> & msgs,qint64 parentId)112 void ResourceScheduler::scheduleItemFetch(const Akonadi::Item &item, const QSet<QByteArray> &parts, const QList<QDBusMessage> &msgs, qint64 parentId)
113
114 {
115 Task t;
116 t.type = FetchItem;
117 t.items << item;
118 t.itemParts = parts;
119 t.dbusMsgs = msgs;
120 t.argument = parentId;
121
122 TaskList &queue = queueForTaskType(t.type);
123 queue << t;
124
125 signalTaskToTracker(t, "FetchItem", QString::number(item.id()));
126 scheduleNext();
127 }
128
scheduleItemsFetch(const Item::List & items,const QSet<QByteArray> & parts,const QDBusMessage & msg)129 void ResourceScheduler::scheduleItemsFetch(const Item::List &items, const QSet<QByteArray> &parts, const QDBusMessage &msg)
130 {
131 Task t;
132 t.type = FetchItems;
133 t.items = items;
134 t.itemParts = parts;
135
136 // if the current task does already fetch the requested item, break here but
137 // keep the dbus message, so we can send the reply later on
138 if (mCurrentTask == t) {
139 mCurrentTask.dbusMsgs << msg;
140 return;
141 }
142
143 // If this task is already in the queue, merge with it.
144 TaskList &queue = queueForTaskType(t.type);
145 const int idx = queue.indexOf(t);
146 if (idx != -1) {
147 queue[idx].dbusMsgs << msg;
148 return;
149 }
150
151 t.dbusMsgs << msg;
152 queue << t;
153
154 QStringList ids;
155 ids.reserve(items.size());
156 for (const auto &item : items) {
157 ids.push_back(QString::number(item.id()));
158 }
159 signalTaskToTracker(t, "FetchItems", ids.join(QLatin1String(", ")));
160 scheduleNext();
161 }
162
scheduleResourceCollectionDeletion()163 void ResourceScheduler::scheduleResourceCollectionDeletion()
164 {
165 Task t;
166 t.type = DeleteResourceCollection;
167 TaskList &queue = queueForTaskType(t.type);
168 if (queue.contains(t) || mCurrentTask == t) {
169 return;
170 }
171 queue << t;
172 signalTaskToTracker(t, "DeleteResourceCollection");
173 scheduleNext();
174 }
175
scheduleCacheInvalidation(const Collection & collection)176 void ResourceScheduler::scheduleCacheInvalidation(const Collection &collection)
177 {
178 Task t;
179 t.type = InvalideCacheForCollection;
180 t.collection = collection;
181 TaskList &queue = queueForTaskType(t.type);
182 if (queue.contains(t) || mCurrentTask == t) {
183 return;
184 }
185 queue << t;
186 signalTaskToTracker(t, "InvalideCacheForCollection", QString::number(collection.id()));
187 scheduleNext();
188 }
189
scheduleChangeReplay()190 void ResourceScheduler::scheduleChangeReplay()
191 {
192 Task t;
193 t.type = ChangeReplay;
194 TaskList &queue = queueForTaskType(t.type);
195 // see ResourceBase::changeProcessed() for why we do not check for mCurrentTask == t here like in the other tasks
196 if (queue.contains(t)) {
197 return;
198 }
199 queue << t;
200 signalTaskToTracker(t, "ChangeReplay");
201 scheduleNext();
202 }
203
scheduleMoveReplay(const Collection & movedCollection,RecursiveMover * mover)204 void ResourceScheduler::scheduleMoveReplay(const Collection &movedCollection, RecursiveMover *mover)
205 {
206 Task t;
207 t.type = RecursiveMoveReplay;
208 t.collection = movedCollection;
209 t.argument = QVariant::fromValue(mover);
210 TaskList &queue = queueForTaskType(t.type);
211
212 if (queue.contains(t) || mCurrentTask == t) {
213 return;
214 }
215
216 queue << t;
217 signalTaskToTracker(t, "RecursiveMoveReplay", QString::number(t.collection.id()));
218 scheduleNext();
219 }
220
scheduleFullSyncCompletion()221 void Akonadi::ResourceScheduler::scheduleFullSyncCompletion()
222 {
223 Task t;
224 t.type = SyncAllDone;
225 TaskList &queue = queueForTaskType(t.type);
226 // no compression here, all this does is emitting a D-Bus signal anyway, and compression can trigger races on the receiver side with the signal being lost
227 queue << t;
228 signalTaskToTracker(t, "SyncAllDone");
229 scheduleNext();
230 }
231
scheduleCollectionTreeSyncCompletion()232 void Akonadi::ResourceScheduler::scheduleCollectionTreeSyncCompletion()
233 {
234 Task t;
235 t.type = SyncCollectionTreeDone;
236 TaskList &queue = queueForTaskType(t.type);
237 // no compression here, all this does is emitting a D-Bus signal anyway, and compression can trigger races on the receiver side with the signal being lost
238 queue << t;
239 signalTaskToTracker(t, "SyncCollectionTreeDone");
240 scheduleNext();
241 }
242
scheduleCustomTask(QObject * receiver,const char * methodName,const QVariant & argument,ResourceBase::SchedulePriority priority)243 void Akonadi::ResourceScheduler::scheduleCustomTask(QObject *receiver,
244 const char *methodName,
245 const QVariant &argument,
246 ResourceBase::SchedulePriority priority)
247 {
248 Task t;
249 t.type = Custom;
250 t.receiver = receiver;
251 t.methodName = methodName;
252 t.argument = argument;
253 QueueType queueType = GenericTaskQueue;
254 if (priority == ResourceBase::AfterChangeReplay) {
255 queueType = AfterChangeReplayQueue;
256 } else if (priority == ResourceBase::Prepend) {
257 queueType = PrependTaskQueue;
258 }
259 TaskList &queue = mTaskList[queueType];
260
261 if (queue.contains(t)) {
262 return;
263 }
264
265 switch (priority) {
266 case ResourceBase::Prepend:
267 queue.prepend(t);
268 break;
269 default:
270 queue.append(t);
271 break;
272 }
273
274 signalTaskToTracker(t, "Custom-" + t.methodName);
275 scheduleNext();
276 }
277
taskDone()278 void ResourceScheduler::taskDone()
279 {
280 if (isEmpty()) {
281 Q_EMIT status(AgentBase::Idle, i18nc("@info:status Application ready for work", "Ready"));
282 }
283
284 if (s_resourcetracker) {
285 const QList<QVariant> argumentList = {QString::number(mCurrentTask.serial), QString()};
286 s_resourcetracker->asyncCallWithArgumentList(QStringLiteral("jobEnded"), argumentList);
287 }
288
289 mCurrentTask = Task();
290 mCurrentTasksQueue = -1;
291 scheduleNext();
292 }
293
itemFetchDone(const QString & msg)294 void ResourceScheduler::itemFetchDone(const QString &msg)
295 {
296 Q_ASSERT(mCurrentTask.type == FetchItem);
297
298 TaskList &queue = queueForTaskType(mCurrentTask.type);
299
300 const qint64 parentId = mCurrentTask.argument.toLongLong();
301 // msg is empty, there was no error
302 if (msg.isEmpty() && !queue.isEmpty()) {
303 Task &nextTask = queue[0];
304 // If the next task is FetchItem too...
305 if (nextTask.type != mCurrentTask.type || nextTask.argument.toLongLong() != parentId) {
306 // If the next task is not FetchItem or the next FetchItem task has
307 // different parentId then this was the last task in the series, so
308 // send the DBus replies.
309 mCurrentTask.sendDBusReplies(msg);
310 }
311 } else {
312 // msg was not empty, there was an error.
313 // remove all subsequent FetchItem tasks with the same parentId
314 auto iter = queue.begin();
315 while (iter != queue.end()) {
316 if (iter->type != mCurrentTask.type || iter->argument.toLongLong() == parentId) {
317 iter = queue.erase(iter);
318 continue;
319 } else {
320 break;
321 }
322 }
323
324 // ... and send DBus reply with the error message
325 mCurrentTask.sendDBusReplies(msg);
326 }
327
328 taskDone();
329 }
330
deferTask()331 void ResourceScheduler::deferTask()
332 {
333 if (mCurrentTask.type == Invalid) {
334 return;
335 }
336
337 if (s_resourcetracker) {
338 const QList<QVariant> argumentList = {QString::number(mCurrentTask.serial), QString()};
339 s_resourcetracker->asyncCallWithArgumentList(QStringLiteral("jobEnded"), argumentList);
340 }
341
342 Task t = mCurrentTask;
343 mCurrentTask = Task();
344
345 Q_ASSERT(mCurrentTasksQueue >= 0 && mCurrentTasksQueue < NQueueCount);
346 mTaskList[mCurrentTasksQueue].prepend(t);
347 mCurrentTasksQueue = -1;
348
349 signalTaskToTracker(t, "DeferedTask");
350
351 scheduleNext();
352 }
353
isEmpty()354 bool ResourceScheduler::isEmpty()
355 {
356 for (int i = 0; i < NQueueCount; ++i) {
357 if (!mTaskList[i].isEmpty()) {
358 return false;
359 }
360 }
361 return true;
362 }
363
scheduleNext()364 void ResourceScheduler::scheduleNext()
365 {
366 if (mCurrentTask.type != Invalid || isEmpty() || !mOnline) {
367 return;
368 }
369 QTimer::singleShot(0s, this, &ResourceScheduler::executeNext);
370 }
371
executeNext()372 void ResourceScheduler::executeNext()
373 {
374 if (mCurrentTask.type != Invalid || isEmpty()) {
375 return;
376 }
377
378 for (int i = 0; i < NQueueCount; ++i) {
379 if (!mTaskList[i].isEmpty()) {
380 mCurrentTask = mTaskList[i].takeFirst();
381 mCurrentTasksQueue = i;
382 break;
383 }
384 }
385
386 if (s_resourcetracker) {
387 const QList<QVariant> argumentList = {QString::number(mCurrentTask.serial)};
388 s_resourcetracker->asyncCallWithArgumentList(QStringLiteral("jobStarted"), argumentList);
389 }
390
391 switch (mCurrentTask.type) {
392 case SyncAll:
393 Q_EMIT executeFullSync();
394 break;
395 case SyncCollectionTree:
396 Q_EMIT executeCollectionTreeSync();
397 break;
398 case SyncCollection:
399 Q_EMIT executeCollectionSync(mCurrentTask.collection);
400 break;
401 case SyncCollectionAttributes:
402 Q_EMIT executeCollectionAttributesSync(mCurrentTask.collection);
403 break;
404 case SyncTags:
405 Q_EMIT executeTagSync();
406 break;
407 case FetchItem:
408 Q_EMIT executeItemFetch(mCurrentTask.items.at(0), mCurrentTask.itemParts);
409 break;
410 case FetchItems:
411 Q_EMIT executeItemsFetch(mCurrentTask.items, mCurrentTask.itemParts);
412 break;
413 case DeleteResourceCollection:
414 Q_EMIT executeResourceCollectionDeletion();
415 break;
416 case InvalideCacheForCollection:
417 Q_EMIT executeCacheInvalidation(mCurrentTask.collection);
418 break;
419 case ChangeReplay:
420 Q_EMIT executeChangeReplay();
421 break;
422 case RecursiveMoveReplay:
423 Q_EMIT executeRecursiveMoveReplay(mCurrentTask.argument.value<RecursiveMover *>());
424 break;
425 case SyncAllDone:
426 Q_EMIT fullSyncComplete();
427 break;
428 case SyncCollectionTreeDone:
429 Q_EMIT collectionTreeSyncComplete();
430 break;
431 case SyncRelations:
432 Q_EMIT executeRelationSync();
433 break;
434 case Custom: {
435 const QByteArray methodSig = mCurrentTask.methodName + QByteArray("(QVariant)");
436 const bool hasSlotWithVariant = mCurrentTask.receiver->metaObject()->indexOfMethod(methodSig.constData()) != -1;
437 bool success = false;
438 if (hasSlotWithVariant) {
439 success = QMetaObject::invokeMethod(mCurrentTask.receiver, mCurrentTask.methodName.constData(), Q_ARG(QVariant, mCurrentTask.argument));
440 Q_ASSERT_X(success || !mCurrentTask.argument.isValid(),
441 "ResourceScheduler::executeNext",
442 "Valid argument was provided but the method wasn't found");
443 }
444 if (!success) {
445 success = QMetaObject::invokeMethod(mCurrentTask.receiver, mCurrentTask.methodName.constData());
446 }
447
448 if (!success) {
449 qCCritical(AKONADIAGENTBASE_LOG) << "Could not invoke slot" << mCurrentTask.methodName << "on" << mCurrentTask.receiver << "with argument"
450 << mCurrentTask.argument;
451 }
452 break;
453 }
454 default: {
455 qCCritical(AKONADIAGENTBASE_LOG) << "Unhandled task type" << mCurrentTask.type;
456 dump();
457 Q_ASSERT(false);
458 }
459 }
460 }
461
currentTask() const462 ResourceScheduler::Task ResourceScheduler::currentTask() const
463 {
464 return mCurrentTask;
465 }
466
currentTask()467 ResourceScheduler::Task &ResourceScheduler::currentTask()
468 {
469 return mCurrentTask;
470 }
471
setOnline(bool state)472 void ResourceScheduler::setOnline(bool state)
473 {
474 if (mOnline == state) {
475 return;
476 }
477 mOnline = state;
478 if (mOnline) {
479 scheduleNext();
480 } else {
481 if (mCurrentTask.type != Invalid) {
482 // abort running task
483 queueForTaskType(mCurrentTask.type).prepend(mCurrentTask);
484 mCurrentTask = Task();
485 mCurrentTasksQueue = -1;
486 }
487 // abort pending synchronous tasks, might take longer until the resource goes online again
488 TaskList &itemFetchQueue = queueForTaskType(FetchItem);
489 qint64 parentId = -1;
490 Task lastTask;
491 for (QList<Task>::iterator it = itemFetchQueue.begin(); it != itemFetchQueue.end();) {
492 if ((*it).type == FetchItem) {
493 qint64 idx = it->argument.toLongLong();
494 if (parentId == -1) {
495 parentId = idx;
496 }
497 if (idx != parentId) {
498 // Only emit the DBus reply once we reach the last taskwith the
499 // same "idx"
500 lastTask.sendDBusReplies(i18nc("@info", "Job canceled."));
501 parentId = idx;
502 }
503 lastTask = (*it);
504 it = itemFetchQueue.erase(it);
505 if (s_resourcetracker) {
506 const QList<QVariant> argumentList = {QString::number(mCurrentTask.serial), i18nc("@info", "Job canceled.")};
507 s_resourcetracker->asyncCallWithArgumentList(QStringLiteral("jobEnded"), argumentList);
508 }
509 } else {
510 ++it;
511 }
512 }
513 }
514 }
515
signalTaskToTracker(const Task & task,const QByteArray & taskType,const QString & debugString)516 void ResourceScheduler::signalTaskToTracker(const Task &task, const QByteArray &taskType, const QString &debugString)
517 {
518 // if there's a job tracer running, tell it about the new job
519 if (!s_resourcetracker) {
520 const QString suffix = Akonadi::Instance::identifier().isEmpty() ? QString() : QLatin1Char('-') + Akonadi::Instance::identifier();
521 if (QDBusConnection::sessionBus().interface()->isServiceRegistered(QStringLiteral("org.kde.akonadiconsole") + suffix)) {
522 s_resourcetracker = new QDBusInterface(QLatin1String("org.kde.akonadiconsole") + suffix,
523 QStringLiteral("/resourcesJobtracker"),
524 QStringLiteral("org.freedesktop.Akonadi.JobTracker"),
525 QDBusConnection::sessionBus(),
526 nullptr);
527 }
528 }
529
530 if (s_resourcetracker) {
531 const QList<QVariant> argumentList = QList<QVariant>() << static_cast<AgentBase *>(parent())->identifier() // "session" (in our case resource)
532 << QString::number(task.serial) // "job"
533 << QString() // "parent job"
534 << QString::fromLatin1(taskType) // "job type"
535 << debugString // "job debugging string"
536 ;
537 s_resourcetracker->asyncCallWithArgumentList(QStringLiteral("jobCreated"), argumentList);
538 }
539 }
540
collectionRemoved(const Akonadi::Collection & collection)541 void ResourceScheduler::collectionRemoved(const Akonadi::Collection &collection)
542 {
543 if (!collection.isValid()) { // should not happen, but you never know...
544 return;
545 }
546 TaskList &queue = queueForTaskType(SyncCollection);
547 for (QList<Task>::iterator it = queue.begin(); it != queue.end();) {
548 if ((*it).type == SyncCollection && (*it).collection == collection) {
549 it = queue.erase(it);
550 qCDebug(AKONADIAGENTBASE_LOG) << " erasing";
551 } else {
552 ++it;
553 }
554 }
555 }
556
sendDBusReplies(const QString & errorMsg)557 void ResourceScheduler::Task::sendDBusReplies(const QString &errorMsg)
558 {
559 for (const QDBusMessage &msg : std::as_const(dbusMsgs)) {
560 qCDebug(AKONADIAGENTBASE_LOG) << "Sending dbus reply for method" << methodName << "with error" << errorMsg;
561 QDBusMessage reply;
562 if (!errorMsg.isEmpty()) {
563 reply = msg.createErrorReply(QDBusError::Failed, errorMsg);
564 } else if (msg.member() == QLatin1String("requestItemDelivery")) {
565 reply = msg.createReply();
566 } else if (msg.member().isEmpty()) {
567 continue; // unittest calls scheduleItemFetch with empty QDBusMessage
568 } else {
569 qCCritical(AKONADIAGENTBASE_LOG) << "ResourceScheduler: got unexpected method name :" << msg.member();
570 }
571 QDBusConnection::sessionBus().send(reply);
572 }
573 }
574
queueTypeForTaskType(TaskType type)575 ResourceScheduler::QueueType ResourceScheduler::queueTypeForTaskType(TaskType type)
576 {
577 switch (type) {
578 case ChangeReplay:
579 case RecursiveMoveReplay:
580 return ChangeReplayQueue;
581 case FetchItem:
582 case FetchItems:
583 case SyncCollectionAttributes:
584 return UserActionQueue;
585 default:
586 return GenericTaskQueue;
587 }
588 }
589
queueForTaskType(TaskType type)590 ResourceScheduler::TaskList &ResourceScheduler::queueForTaskType(TaskType type)
591 {
592 const QueueType qt = queueTypeForTaskType(type);
593 return mTaskList[qt];
594 }
595
dump() const596 void ResourceScheduler::dump() const
597 {
598 qCDebug(AKONADIAGENTBASE_LOG) << dumpToString();
599 }
600
dumpToString() const601 QString ResourceScheduler::dumpToString() const
602 {
603 QString ret;
604 QTextStream str(&ret);
605 str << "ResourceScheduler: " << (mOnline ? "Online" : "Offline") << '\n';
606 str << " current task: " << mCurrentTask << '\n';
607 for (int i = 0; i < NQueueCount; ++i) {
608 const TaskList &queue = mTaskList[i];
609 if (queue.isEmpty()) {
610 str << " queue " << i << " is empty" << '\n';
611 } else {
612 str << " queue " << i << " " << queue.size() << " tasks:\n";
613 const QList<Task>::const_iterator queueEnd(queue.constEnd());
614 for (QList<Task>::const_iterator it = queue.constBegin(); it != queueEnd; ++it) {
615 str << " " << (*it) << '\n';
616 }
617 }
618 }
619 str.flush();
620 return ret;
621 }
622
clear()623 void ResourceScheduler::clear()
624 {
625 qCDebug(AKONADIAGENTBASE_LOG) << "Clearing ResourceScheduler queues:";
626 for (int i = 0; i < NQueueCount; ++i) {
627 TaskList &queue = mTaskList[i];
628 queue.clear();
629 }
630 mCurrentTask = Task();
631 mCurrentTasksQueue = -1;
632 }
633
cancelQueues()634 void Akonadi::ResourceScheduler::cancelQueues()
635 {
636 for (int i = 0; i < NQueueCount; ++i) {
637 TaskList &queue = mTaskList[i];
638 if (s_resourcetracker) {
639 for (const Task &t : queue) {
640 QList<QVariant> argumentList{QString::number(t.serial), QString()};
641 s_resourcetracker->asyncCallWithArgumentList(QStringLiteral("jobEnded"), argumentList);
642 }
643 }
644 queue.clear();
645 }
646 }
647
648 static const char s_taskTypes[][27] = {"Invalid (no task)",
649 "SyncAll",
650 "SyncCollectionTree",
651 "SyncCollection",
652 "SyncCollectionAttributes",
653 "SyncTags",
654 "FetchItem",
655 "FetchItems",
656 "ChangeReplay",
657 "RecursiveMoveReplay",
658 "DeleteResourceCollection",
659 "InvalideCacheForCollection",
660 "SyncAllDone",
661 "SyncCollectionTreeDone",
662 "SyncRelations",
663 "Custom"};
664
operator <<(QTextStream & d,const ResourceScheduler::Task & task)665 QTextStream &Akonadi::operator<<(QTextStream &d, const ResourceScheduler::Task &task)
666 {
667 d << task.serial << " " << s_taskTypes[task.type] << " ";
668 if (task.type != ResourceScheduler::Invalid) {
669 if (task.collection.isValid()) {
670 d << "collection " << task.collection.id() << " ";
671 }
672 if (!task.items.isEmpty()) {
673 QStringList ids;
674 ids.reserve(task.items.size());
675 for (const auto &item : std::as_const(task.items)) {
676 ids.push_back(QString::number(item.id()));
677 }
678 d << "items " << ids.join(QLatin1String(", ")) << " ";
679 }
680 if (!task.methodName.isEmpty()) {
681 d << task.methodName << " " << task.argument.toString();
682 }
683 }
684 return d;
685 }
686
operator <<(QDebug d,const ResourceScheduler::Task & task)687 QDebug Akonadi::operator<<(QDebug d, const ResourceScheduler::Task &task)
688 {
689 QString s;
690 QTextStream str(&s);
691 str << task;
692 d << s;
693 return d;
694 }
695
696 /// @endcond
697
698 #include "moc_resourcescheduler_p.cpp"
699