https://bugs.kde.org/show_bug.cgi?id=526550
Bug ID: 526550
Summary: Pending jobs of an aborted resource task still
complete the next task (clearCurrentTaskPendingJobs()
does not disconnect them)
Classification: Frameworks and Libraries
Product: Akonadi
Version First 6.8.1
Reported In:
Platform: Other
OS: Linux
Status: REPORTED
Severity: normal
Priority: NOR
Component: libakonadi
Assignee: [email protected]
Reporter: [email protected]
CC: [email protected]
Target Milestone: ---
SUMMARY
ResourceScheduler::clearCurrentTaskPendingJobs() is meant to detach the pending
jobs of
an aborted task, but the disconnect() call it uses does not remove the
connections from
those jobs to ResourceBase. A commit or payload store that belongs to an
aborted task
therefore still completes whichever task runs next.
Where: src/agentbase/resourcescheduler.cpp (current master):
void ResourceScheduler::clearCurrentTaskPendingJobs()
{
for (auto *job : std::as_const(mCurrentTaskPendingJobs)) {
// Disconnect this job completely and let it complete
// without disturbing anything else
disconnect(job, nullptr);
}
mCurrentTaskPendingJobs.clear();
}
Inside a member function, disconnect(job, nullptr) resolves to
QObject::disconnect(const QObject *receiver, const char *method) const, i.e. it
removes
the connections from the scheduler's own signals to the job. The job's
connections to
ResourceBasePrivate::changeCommittedResult, slotItemSyncDone, slotDeliveryDone,
slotCollectionSyncDone and so on stay in place, and so does the scheduler's own
connection to the job's finished signal (see the test below).
STEPS TO REPRODUCE
Part 1, the disconnect() call on its own (run as shown, Qt 6.11.2). Scheduler
stands
for ResourceScheduler, the timer for the pending KJob:
#include <QCoreApplication>
#include <QTimer>
#include <cstdio>
// Stands for ResourceScheduler: clearPending() is the loop body of
// ResourceScheduler::clearCurrentTaskPendingJobs().
class Scheduler : public QObject
{
public:
void clearPending(QObject *job)
{
disconnect(job, nullptr);
}
};
int main(int argc, char **argv)
{
QCoreApplication app(argc, argv);
Scheduler scheduler;
QObject resourceBase; // stands for ResourceBasePrivate
QTimer job; // stands for the pending KJob, timeout() for result()
job.setSingleShot(true);
QObject::connect(&job, &QTimer::timeout, &resourceBase, [] {
std::puts("result still delivered to ResourceBase");
});
QObject::connect(&job, &QTimer::timeout, &scheduler, [] {
std::puts("result still delivered to the scheduler");
});
scheduler.clearPending(&job);
job.start(0);
QTimer::singleShot(50, &app, &QCoreApplication::quit);
return app.exec();
}
Output:
result still delivered to ResourceBase
result still delivered to the scheduler
Part 2, in a resource. The sequence that leads there:
1. A resource task has a pending job: the TransactionSequence of
changesCommitted(),
the ItemModifyJob of itemRetrieved(), the TransactionSequence of
itemsRetrieved() during a FetchItems task, or an ItemSync.
2. The resource goes offline before that job finishes
(ResourceScheduler::setOnline(false)
puts the task back at the front of its queue and calls clearCurrentTask()),
e.g. via
setTemporaryOffline() or a network change.
3. The resource comes back online before the old job finishes, and the requeued
task (or
another one) starts.
4. The old job finishes.
In normal operation this needs a network change at the wrong moment. To force
it, a
resource can go offline and online right after committing a change (a sketch, I
have not
run it in this form):
void MyResource::itemChanged(const Akonadi::Item &item, const
QSet<QByteArray> &)
{
qDebug() << "replaying change of item" << item.id();
changeCommitted(item); // queues the TransactionSequence as a pending
job
if (!mToggled) {
mToggled = true;
setOnline(false); // requeues the ChangeReplay task, "clears" the
pending job
setOnline(true); // the task starts again while the commit is still
running
}
}
Modifying one item of that resource should then log the same change twice, and
the first
commit ends the second run.
OBSERVED RESULT
Part 1 is the output of a test. The consequences below follow from reading
resourcebase.cpp and resourcescheduler.cpp; I have not reproduced them end to
end in a
running resource.
The old job's result handler runs against the task that is current now:
- Change replay: the requeued notification is replayed a second time; the old
commit then
calls changeProcessed(), which dequeues it and ends the new run. A commit
that arrives
after that dequeues whatever notification is at the head of the queue by
then, whether
or not its handler has finished, so a change can be dropped without any
error.
- Item retrieval: slotItemSyncDone ends the new run early. If the resource then
delivers
its result while no FetchItems task is current, itemsRetrieved() falls back
to a full
ItemSync on currentCollection(), which removes every other item of that
collection
from the local cache. The Q_ASSERT_X in createItemSyncInstanceIfMissing()
would catch
this, but does nothing in release builds.
EXPECTED RESULT
A job that belonged to an aborted task finishes without touching the scheduler
or the
task that is running by then, as the comment in the function says.
ADDITIONAL INFORMATION
A fix has to do a little more than correct the disconnect() call:
- The receivers are ResourceBasePrivate and ResourceBase, which the scheduler
does not
know about. Either the scheduler tells ResourceBase which jobs it dropped (so
that it
can QObject::disconnect(job, nullptr, receiver, nullptr)), or the result
handlers
check that the task they were started for is still the current one (the task
serial
would do).
- A plain QObject::disconnect(job, nullptr, nullptr, nullptr) is not an option:
it also
cuts the Session's own connection to the job and stalls the session's queue.
- Once the handlers no longer run, ResourceBasePrivate::mItemSyncer (reset only
in
slotItemSyncDone) and probably mCollectionSyncer would keep pointing at the
job of
the aborted task, so the state of the aborted task has to be reset at the
same place.
I found this while writing the Microsoft 365 (Graph) resource in
kdepim-runtime, which
uses setTemporaryOffline() to retry changes the server did not take. As a
workaround the
resource queues a marker job on the default session when it goes offline and
keeps its
scheduler stopped until that job has finished (the session runs its jobs in
order, so
every earlier one is through by then). Other resources that go offline while a
task is
running take the same path.
I can turn this into a merge request if you tell me which of the two approaches
you prefer.
SOFTWARE/OS VERSIONS
Akonadi: 6.8.1 (KDE Gear 26.08.1; code unchanged on master as of 2026-10-02)
KDE Frameworks: 6.30.0
Qt: 6.11.2
OS: Arch Linux (CachyOS)
--
You are receiving this mail because:
You are watching all bug changes.