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