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.

Reply via email to