File indexing completed on 2025-01-05 04:46:26

0001 /*
0002     SPDX-FileCopyrightText: 2007 Volker Krause <vkrause@kde.org>
0003 
0004     SPDX-License-Identifier: LGPL-2.0-or-later
0005 */
0006 
0007 #include "resourcescheduler_p.h"
0008 
0009 #include "recursivemover_p.h"
0010 #include <QDBusConnection>
0011 
0012 #include "akonadiagentbase_debug.h"
0013 #include "private/instance_p.h"
0014 #include <KLocalizedString>
0015 
0016 #include <QDBusInterface>
0017 #include <QTimer>
0018 
0019 using namespace Akonadi;
0020 using namespace std::chrono_literals;
0021 qint64 ResourceScheduler::Task::latestSerial = 0;
0022 static QDBusAbstractInterface *s_resourcetracker = nullptr;
0023 
0024 /// @cond PRIVATE
0025 
0026 ResourceScheduler::ResourceScheduler(QObject *parent)
0027     : QObject(parent)
0028 {
0029 }
0030 
0031 void ResourceScheduler::scheduleFullSync()
0032 {
0033     Task t;
0034     t.type = SyncAll;
0035     TaskList &queue = queueForTaskType(t.type);
0036     if (queue.contains(t) || mCurrentTask == t) {
0037         return;
0038     }
0039     queue << t;
0040     signalTaskToTracker(t, "SyncAll");
0041     scheduleNext();
0042 }
0043 
0044 void ResourceScheduler::scheduleCollectionTreeSync()
0045 {
0046     Task t;
0047     t.type = SyncCollectionTree;
0048     TaskList &queue = queueForTaskType(t.type);
0049     if (queue.contains(t) || mCurrentTask == t) {
0050         return;
0051     }
0052     queue << t;
0053     signalTaskToTracker(t, "SyncCollectionTree");
0054     scheduleNext();
0055 }
0056 
0057 void ResourceScheduler::scheduleTagSync()
0058 {
0059     Task t;
0060     t.type = SyncTags;
0061     TaskList &queue = queueForTaskType(t.type);
0062     if (queue.contains(t) || mCurrentTask == t) {
0063         return;
0064     }
0065     queue << t;
0066     signalTaskToTracker(t, "SyncTags");
0067     scheduleNext();
0068 }
0069 
0070 void ResourceScheduler::scheduleRelationSync()
0071 {
0072     Task t;
0073     t.type = SyncRelations;
0074     TaskList &queue = queueForTaskType(t.type);
0075     if (queue.contains(t) || mCurrentTask == t) {
0076         return;
0077     }
0078     queue << t;
0079     signalTaskToTracker(t, "SyncRelations");
0080     scheduleNext();
0081 }
0082 
0083 void ResourceScheduler::scheduleSync(const Collection &col)
0084 {
0085     Task t;
0086     t.type = SyncCollection;
0087     t.collection = col;
0088     TaskList &queue = queueForTaskType(t.type);
0089     if (queue.contains(t) || mCurrentTask == t) {
0090         return;
0091     }
0092     queue << t;
0093     signalTaskToTracker(t, "SyncCollection", QString::number(col.id()));
0094     scheduleNext();
0095 }
0096 
0097 void ResourceScheduler::scheduleAttributesSync(const Collection &collection)
0098 {
0099     Task t;
0100     t.type = SyncCollectionAttributes;
0101     t.collection = collection;
0102 
0103     TaskList &queue = queueForTaskType(t.type);
0104     if (queue.contains(t) || mCurrentTask == t) {
0105         return;
0106     }
0107     queue << t;
0108     signalTaskToTracker(t, "SyncCollectionAttributes", QString::number(collection.id()));
0109     scheduleNext();
0110 }
0111 
0112 void ResourceScheduler::scheduleItemFetch(const Akonadi::Item &item, const QSet<QByteArray> &parts, const QList<QDBusMessage> &msgs, qint64 parentId)
0113 
0114 {
0115     Task t;
0116     t.type = FetchItem;
0117     t.items << item;
0118     t.itemParts = parts;
0119     t.dbusMsgs = msgs;
0120     t.argument = parentId;
0121 
0122     TaskList &queue = queueForTaskType(t.type);
0123     queue << t;
0124 
0125     signalTaskToTracker(t, "FetchItem", QString::number(item.id()));
0126     scheduleNext();
0127 }
0128 
0129 void ResourceScheduler::scheduleItemsFetch(const Item::List &items, const QSet<QByteArray> &parts, const QDBusMessage &msg)
0130 {
0131     Task t;
0132     t.type = FetchItems;
0133     t.items = items;
0134     t.itemParts = parts;
0135 
0136     // if the current task does already fetch the requested item, break here but
0137     // keep the dbus message, so we can send the reply later on
0138     if (mCurrentTask == t) {
0139         mCurrentTask.dbusMsgs << msg;
0140         return;
0141     }
0142 
0143     // If this task is already in the queue, merge with it.
0144     TaskList &queue = queueForTaskType(t.type);
0145     const int idx = queue.indexOf(t);
0146     if (idx != -1) {
0147         queue[idx].dbusMsgs << msg;
0148         return;
0149     }
0150 
0151     t.dbusMsgs << msg;
0152     queue << t;
0153 
0154     QStringList ids;
0155     ids.reserve(items.size());
0156     for (const auto &item : items) {
0157         ids.push_back(QString::number(item.id()));
0158     }
0159     signalTaskToTracker(t, "FetchItems", ids.join(QLatin1StringView(", ")));
0160     scheduleNext();
0161 }
0162 
0163 void ResourceScheduler::scheduleResourceCollectionDeletion()
0164 {
0165     Task t;
0166     t.type = DeleteResourceCollection;
0167     TaskList &queue = queueForTaskType(t.type);
0168     if (queue.contains(t) || mCurrentTask == t) {
0169         return;
0170     }
0171     queue << t;
0172     signalTaskToTracker(t, "DeleteResourceCollection");
0173     scheduleNext();
0174 }
0175 
0176 void ResourceScheduler::scheduleCacheInvalidation(const Collection &collection)
0177 {
0178     Task t;
0179     t.type = InvalideCacheForCollection;
0180     t.collection = collection;
0181     TaskList &queue = queueForTaskType(t.type);
0182     if (queue.contains(t) || mCurrentTask == t) {
0183         return;
0184     }
0185     queue << t;
0186     signalTaskToTracker(t, "InvalideCacheForCollection", QString::number(collection.id()));
0187     scheduleNext();
0188 }
0189 
0190 void ResourceScheduler::scheduleChangeReplay()
0191 {
0192     Task t;
0193     t.type = ChangeReplay;
0194     TaskList &queue = queueForTaskType(t.type);
0195     // see ResourceBase::changeProcessed() for why we do not check for mCurrentTask == t here like in the other tasks
0196     if (queue.contains(t)) {
0197         return;
0198     }
0199     queue << t;
0200     signalTaskToTracker(t, "ChangeReplay");
0201     scheduleNext();
0202 }
0203 
0204 void ResourceScheduler::scheduleMoveReplay(const Collection &movedCollection, RecursiveMover *mover)
0205 {
0206     Task t;
0207     t.type = RecursiveMoveReplay;
0208     t.collection = movedCollection;
0209     t.argument = QVariant::fromValue(mover);
0210     TaskList &queue = queueForTaskType(t.type);
0211 
0212     if (queue.contains(t) || mCurrentTask == t) {
0213         return;
0214     }
0215 
0216     queue << t;
0217     signalTaskToTracker(t, "RecursiveMoveReplay", QString::number(t.collection.id()));
0218     scheduleNext();
0219 }
0220 
0221 void Akonadi::ResourceScheduler::scheduleFullSyncCompletion()
0222 {
0223     Task t;
0224     t.type = SyncAllDone;
0225     TaskList &queue = queueForTaskType(t.type);
0226     // 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
0227     queue << t;
0228     signalTaskToTracker(t, "SyncAllDone");
0229     scheduleNext();
0230 }
0231 
0232 void Akonadi::ResourceScheduler::scheduleCollectionTreeSyncCompletion()
0233 {
0234     Task t;
0235     t.type = SyncCollectionTreeDone;
0236     TaskList &queue = queueForTaskType(t.type);
0237     // 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
0238     queue << t;
0239     signalTaskToTracker(t, "SyncCollectionTreeDone");
0240     scheduleNext();
0241 }
0242 
0243 void Akonadi::ResourceScheduler::scheduleCustomTask(QObject *receiver,
0244                                                     const char *methodName,
0245                                                     const QVariant &argument,
0246                                                     ResourceBase::SchedulePriority priority)
0247 {
0248     Task t;
0249     t.type = Custom;
0250     t.receiver = receiver;
0251     t.methodName = methodName;
0252     t.argument = argument;
0253     QueueType queueType = GenericTaskQueue;
0254     if (priority == ResourceBase::AfterChangeReplay) {
0255         queueType = AfterChangeReplayQueue;
0256     } else if (priority == ResourceBase::Prepend) {
0257         queueType = PrependTaskQueue;
0258     }
0259     TaskList &queue = mTaskList[queueType];
0260 
0261     if (queue.contains(t)) {
0262         return;
0263     }
0264 
0265     switch (priority) {
0266     case ResourceBase::Prepend:
0267         queue.prepend(t);
0268         break;
0269     default:
0270         queue.append(t);
0271         break;
0272     }
0273 
0274     signalTaskToTracker(t, "Custom-" + t.methodName);
0275     scheduleNext();
0276 }
0277 
0278 void ResourceScheduler::taskDone()
0279 {
0280     if (isEmpty()) {
0281         Q_EMIT status(AgentBase::Idle, i18nc("@info:status Application ready for work", "Ready"));
0282     }
0283 
0284     if (s_resourcetracker) {
0285         const QList<QVariant> argumentList = {QString::number(mCurrentTask.serial), QString()};
0286         s_resourcetracker->asyncCallWithArgumentList(QStringLiteral("jobEnded"), argumentList);
0287     }
0288 
0289     mCurrentTask = Task();
0290     mCurrentTasksQueue = -1;
0291     scheduleNext();
0292 }
0293 
0294 void ResourceScheduler::itemFetchDone(const QString &msg)
0295 {
0296     Q_ASSERT(mCurrentTask.type == FetchItem);
0297 
0298     TaskList &queue = queueForTaskType(mCurrentTask.type);
0299 
0300     const qint64 parentId = mCurrentTask.argument.toLongLong();
0301     // msg is empty, there was no error
0302     if (msg.isEmpty() && !queue.isEmpty()) {
0303         Task &nextTask = queue[0];
0304         // If the next task is FetchItem too...
0305         if (nextTask.type != mCurrentTask.type || nextTask.argument.toLongLong() != parentId) {
0306             // If the next task is not FetchItem or the next FetchItem task has
0307             // different parentId then this was the last task in the series, so
0308             // send the DBus replies.
0309             mCurrentTask.sendDBusReplies(msg);
0310         }
0311     } else {
0312         // msg was not empty, there was an error.
0313         // remove all subsequent FetchItem tasks with the same parentId
0314         auto iter = queue.begin();
0315         while (iter != queue.end()) {
0316             if (iter->type != mCurrentTask.type || iter->argument.toLongLong() == parentId) {
0317                 iter = queue.erase(iter);
0318                 continue;
0319             } else {
0320                 break;
0321             }
0322         }
0323 
0324         // ... and send DBus reply with the error message
0325         mCurrentTask.sendDBusReplies(msg);
0326     }
0327 
0328     taskDone();
0329 }
0330 
0331 void ResourceScheduler::deferTask()
0332 {
0333     if (mCurrentTask.type == Invalid) {
0334         return;
0335     }
0336 
0337     if (s_resourcetracker) {
0338         const QList<QVariant> argumentList = {QString::number(mCurrentTask.serial), QString()};
0339         s_resourcetracker->asyncCallWithArgumentList(QStringLiteral("jobEnded"), argumentList);
0340     }
0341 
0342     Task t = mCurrentTask;
0343     mCurrentTask = Task();
0344 
0345     Q_ASSERT(mCurrentTasksQueue >= 0 && mCurrentTasksQueue < NQueueCount);
0346     mTaskList[mCurrentTasksQueue].prepend(t);
0347     mCurrentTasksQueue = -1;
0348 
0349     signalTaskToTracker(t, "DeferedTask");
0350 
0351     scheduleNext();
0352 }
0353 
0354 bool ResourceScheduler::isEmpty()
0355 {
0356     for (int i = 0; i < NQueueCount; ++i) {
0357         if (!mTaskList[i].isEmpty()) {
0358             return false;
0359         }
0360     }
0361     return true;
0362 }
0363 
0364 void ResourceScheduler::scheduleNext()
0365 {
0366     if (mCurrentTask.type != Invalid || isEmpty() || !mOnline) {
0367         return;
0368     }
0369     QTimer::singleShot(0s, this, &ResourceScheduler::executeNext);
0370 }
0371 
0372 void ResourceScheduler::executeNext()
0373 {
0374     if (mCurrentTask.type != Invalid || isEmpty()) {
0375         return;
0376     }
0377 
0378     for (int i = 0; i < NQueueCount; ++i) {
0379         if (!mTaskList[i].isEmpty()) {
0380             mCurrentTask = mTaskList[i].takeFirst();
0381             mCurrentTasksQueue = i;
0382             break;
0383         }
0384     }
0385 
0386     if (s_resourcetracker) {
0387         const QList<QVariant> argumentList = {QString::number(mCurrentTask.serial)};
0388         s_resourcetracker->asyncCallWithArgumentList(QStringLiteral("jobStarted"), argumentList);
0389     }
0390 
0391     switch (mCurrentTask.type) {
0392     case SyncAll:
0393         Q_EMIT executeFullSync();
0394         break;
0395     case SyncCollectionTree:
0396         Q_EMIT executeCollectionTreeSync();
0397         break;
0398     case SyncCollection:
0399         Q_EMIT executeCollectionSync(mCurrentTask.collection);
0400         break;
0401     case SyncCollectionAttributes:
0402         Q_EMIT executeCollectionAttributesSync(mCurrentTask.collection);
0403         break;
0404     case SyncTags:
0405         Q_EMIT executeTagSync();
0406         break;
0407     case FetchItem:
0408         Q_EMIT executeItemFetch(mCurrentTask.items.at(0), mCurrentTask.itemParts);
0409         break;
0410     case FetchItems:
0411         Q_EMIT executeItemsFetch(mCurrentTask.items, mCurrentTask.itemParts);
0412         break;
0413     case DeleteResourceCollection:
0414         Q_EMIT executeResourceCollectionDeletion();
0415         break;
0416     case InvalideCacheForCollection:
0417         Q_EMIT executeCacheInvalidation(mCurrentTask.collection);
0418         break;
0419     case ChangeReplay:
0420         Q_EMIT executeChangeReplay();
0421         break;
0422     case RecursiveMoveReplay:
0423         Q_EMIT executeRecursiveMoveReplay(mCurrentTask.argument.value<RecursiveMover *>());
0424         break;
0425     case SyncAllDone:
0426         Q_EMIT fullSyncComplete();
0427         break;
0428     case SyncCollectionTreeDone:
0429         Q_EMIT collectionTreeSyncComplete();
0430         break;
0431     case SyncRelations:
0432         Q_EMIT executeRelationSync();
0433         break;
0434     case Custom: {
0435         const QByteArray methodSig = mCurrentTask.methodName + QByteArray("(QVariant)");
0436         const bool hasSlotWithVariant = mCurrentTask.receiver->metaObject()->indexOfMethod(methodSig.constData()) != -1;
0437         bool success = false;
0438         if (hasSlotWithVariant) {
0439             success = QMetaObject::invokeMethod(mCurrentTask.receiver, mCurrentTask.methodName.constData(), Q_ARG(QVariant, mCurrentTask.argument));
0440             Q_ASSERT_X(success || !mCurrentTask.argument.isValid(),
0441                        "ResourceScheduler::executeNext",
0442                        "Valid argument was provided but the method wasn't found");
0443         }
0444         if (!success) {
0445             success = QMetaObject::invokeMethod(mCurrentTask.receiver, mCurrentTask.methodName.constData());
0446         }
0447 
0448         if (!success) {
0449             qCCritical(AKONADIAGENTBASE_LOG) << "Could not invoke slot" << mCurrentTask.methodName << "on" << mCurrentTask.receiver << "with argument"
0450                                              << mCurrentTask.argument;
0451         }
0452         break;
0453     }
0454     default: {
0455         qCCritical(AKONADIAGENTBASE_LOG) << "Unhandled task type" << mCurrentTask.type;
0456         dump();
0457         Q_ASSERT(false);
0458     }
0459     }
0460 }
0461 
0462 ResourceScheduler::Task ResourceScheduler::currentTask() const
0463 {
0464     return mCurrentTask;
0465 }
0466 
0467 ResourceScheduler::Task &ResourceScheduler::currentTask()
0468 {
0469     return mCurrentTask;
0470 }
0471 
0472 void ResourceScheduler::setOnline(bool state)
0473 {
0474     if (mOnline == state) {
0475         return;
0476     }
0477     mOnline = state;
0478     if (mOnline) {
0479         scheduleNext();
0480     } else {
0481         if (mCurrentTask.type != Invalid) {
0482             // abort running task
0483             queueForTaskType(mCurrentTask.type).prepend(mCurrentTask);
0484             mCurrentTask = Task();
0485             mCurrentTasksQueue = -1;
0486         }
0487         // abort pending synchronous tasks, might take longer until the resource goes online again
0488         TaskList &itemFetchQueue = queueForTaskType(FetchItem);
0489         qint64 parentId = -1;
0490         Task lastTask;
0491         for (QList<Task>::iterator it = itemFetchQueue.begin(); it != itemFetchQueue.end();) {
0492             if ((*it).type == FetchItem) {
0493                 qint64 idx = it->argument.toLongLong();
0494                 if (parentId == -1) {
0495                     parentId = idx;
0496                 }
0497                 if (idx != parentId) {
0498                     // Only emit the DBus reply once we reach the last taskwith the
0499                     // same "idx"
0500                     lastTask.sendDBusReplies(i18nc("@info", "Job canceled."));
0501                     parentId = idx;
0502                 }
0503                 lastTask = (*it);
0504                 it = itemFetchQueue.erase(it);
0505                 if (s_resourcetracker) {
0506                     const QList<QVariant> argumentList = {QString::number(mCurrentTask.serial), i18nc("@info", "Job canceled.")};
0507                     s_resourcetracker->asyncCallWithArgumentList(QStringLiteral("jobEnded"), argumentList);
0508                 }
0509             } else {
0510                 ++it;
0511             }
0512         }
0513     }
0514 }
0515 
0516 void ResourceScheduler::signalTaskToTracker(const Task &task, const QByteArray &taskType, const QString &debugString)
0517 {
0518     // if there's a job tracer running, tell it about the new job
0519     if (!s_resourcetracker) {
0520         const QString suffix = Akonadi::Instance::identifier().isEmpty() ? QString() : QLatin1Char('-') + Akonadi::Instance::identifier();
0521         if (QDBusConnection::sessionBus().interface()->isServiceRegistered(QStringLiteral("org.kde.akonadiconsole") + suffix)) {
0522             s_resourcetracker = new QDBusInterface(QLatin1StringView("org.kde.akonadiconsole") + suffix,
0523                                                    QStringLiteral("/resourcesJobtracker"),
0524                                                    QStringLiteral("org.freedesktop.Akonadi.JobTracker"),
0525                                                    QDBusConnection::sessionBus(),
0526                                                    nullptr);
0527         }
0528     }
0529 
0530     if (s_resourcetracker) {
0531         const QList<QVariant> argumentList = QList<QVariant>() << static_cast<AgentBase *>(parent())->identifier() // "session" (in our case resource)
0532                                                                << QString::number(task.serial) // "job"
0533                                                                << QString() // "parent job"
0534                                                                << QString::fromLatin1(taskType) // "job type"
0535                                                                << debugString // "job debugging string"
0536             ;
0537         s_resourcetracker->asyncCallWithArgumentList(QStringLiteral("jobCreated"), argumentList);
0538     }
0539 }
0540 
0541 void ResourceScheduler::collectionRemoved(const Akonadi::Collection &collection)
0542 {
0543     if (!collection.isValid()) { // should not happen, but you never know...
0544         return;
0545     }
0546     TaskList &queue = queueForTaskType(SyncCollection);
0547     for (QList<Task>::iterator it = queue.begin(); it != queue.end();) {
0548         if ((*it).type == SyncCollection && (*it).collection == collection) {
0549             it = queue.erase(it);
0550             qCDebug(AKONADIAGENTBASE_LOG) << " erasing";
0551         } else {
0552             ++it;
0553         }
0554     }
0555 }
0556 
0557 void ResourceScheduler::Task::sendDBusReplies(const QString &errorMsg)
0558 {
0559     for (const QDBusMessage &msg : std::as_const(dbusMsgs)) {
0560         qCDebug(AKONADIAGENTBASE_LOG) << "Sending dbus reply for method" << methodName << "with error" << errorMsg;
0561         QDBusMessage reply;
0562         if (!errorMsg.isEmpty()) {
0563             reply = msg.createErrorReply(QDBusError::Failed, errorMsg);
0564         } else if (msg.member() == QLatin1StringView("requestItemDelivery")) {
0565             reply = msg.createReply();
0566         } else if (msg.member().isEmpty()) {
0567             continue; // unittest calls scheduleItemFetch with empty QDBusMessage
0568         } else {
0569             qCCritical(AKONADIAGENTBASE_LOG) << "ResourceScheduler: got unexpected method name :" << msg.member();
0570         }
0571         QDBusConnection::sessionBus().send(reply);
0572     }
0573 }
0574 
0575 ResourceScheduler::QueueType ResourceScheduler::queueTypeForTaskType(TaskType type)
0576 {
0577     switch (type) {
0578     case ChangeReplay:
0579     case RecursiveMoveReplay:
0580         return ChangeReplayQueue;
0581     case FetchItem:
0582     case FetchItems:
0583     case SyncCollectionAttributes:
0584         return UserActionQueue;
0585     default:
0586         return GenericTaskQueue;
0587     }
0588 }
0589 
0590 ResourceScheduler::TaskList &ResourceScheduler::queueForTaskType(TaskType type)
0591 {
0592     const QueueType qt = queueTypeForTaskType(type);
0593     return mTaskList[qt];
0594 }
0595 
0596 void ResourceScheduler::dump() const
0597 {
0598     qCDebug(AKONADIAGENTBASE_LOG) << dumpToString();
0599 }
0600 
0601 QString ResourceScheduler::dumpToString() const
0602 {
0603     QString ret;
0604     QTextStream str(&ret);
0605     str << "ResourceScheduler: " << (mOnline ? "Online" : "Offline") << '\n';
0606     str << " current task: " << mCurrentTask << '\n';
0607     for (int i = 0; i < NQueueCount; ++i) {
0608         const TaskList &queue = mTaskList[i];
0609         if (queue.isEmpty()) {
0610             str << " queue " << i << " is empty" << '\n';
0611         } else {
0612             str << " queue " << i << " " << queue.size() << " tasks:\n";
0613             const QList<Task>::const_iterator queueEnd(queue.constEnd());
0614             for (QList<Task>::const_iterator it = queue.constBegin(); it != queueEnd; ++it) {
0615                 str << "  " << (*it) << '\n';
0616             }
0617         }
0618     }
0619     str.flush();
0620     return ret;
0621 }
0622 
0623 void ResourceScheduler::clear()
0624 {
0625     qCDebug(AKONADIAGENTBASE_LOG) << "Clearing ResourceScheduler queues:";
0626     for (int i = 0; i < NQueueCount; ++i) {
0627         TaskList &queue = mTaskList[i];
0628         queue.clear();
0629     }
0630     mCurrentTask = Task();
0631     mCurrentTasksQueue = -1;
0632 }
0633 
0634 void Akonadi::ResourceScheduler::cancelQueues()
0635 {
0636     for (int i = 0; i < NQueueCount; ++i) {
0637         TaskList &queue = mTaskList[i];
0638         if (s_resourcetracker) {
0639             for (const Task &t : queue) {
0640                 QList<QVariant> argumentList{QString::number(t.serial), QString()};
0641                 s_resourcetracker->asyncCallWithArgumentList(QStringLiteral("jobEnded"), argumentList);
0642             }
0643         }
0644         queue.clear();
0645     }
0646 }
0647 
0648 static const char s_taskTypes[][27] = {"Invalid (no task)",
0649                                        "SyncAll",
0650                                        "SyncCollectionTree",
0651                                        "SyncCollection",
0652                                        "SyncCollectionAttributes",
0653                                        "SyncTags",
0654                                        "FetchItem",
0655                                        "FetchItems",
0656                                        "ChangeReplay",
0657                                        "RecursiveMoveReplay",
0658                                        "DeleteResourceCollection",
0659                                        "InvalideCacheForCollection",
0660                                        "SyncAllDone",
0661                                        "SyncCollectionTreeDone",
0662                                        "SyncRelations",
0663                                        "Custom"};
0664 
0665 QTextStream &Akonadi::operator<<(QTextStream &d, const ResourceScheduler::Task &task)
0666 {
0667     d << task.serial << " " << s_taskTypes[task.type] << " ";
0668     if (task.type != ResourceScheduler::Invalid) {
0669         if (task.collection.isValid()) {
0670             d << "collection " << task.collection.id() << " ";
0671         }
0672         if (!task.items.isEmpty()) {
0673             QStringList ids;
0674             ids.reserve(task.items.size());
0675             for (const auto &item : std::as_const(task.items)) {
0676                 ids.push_back(QString::number(item.id()));
0677             }
0678             d << "items " << ids.join(QLatin1StringView(", ")) << " ";
0679         }
0680         if (!task.methodName.isEmpty()) {
0681             d << task.methodName << " " << task.argument.toString();
0682         }
0683     }
0684     return d;
0685 }
0686 
0687 QDebug Akonadi::operator<<(QDebug d, const ResourceScheduler::Task &task)
0688 {
0689     QString s;
0690     QTextStream str(&s);
0691     str << task;
0692     d << s;
0693     return d;
0694 }
0695 
0696 /// @endcond
0697 
0698 #include "moc_resourcescheduler_p.cpp"