Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions lib/composer/composer/autoload_classmap.php
Original file line number Diff line number Diff line change
Expand Up @@ -872,6 +872,7 @@
'OCP\\TaskProcessing\\IProvider' => $baseDir . '/lib/public/TaskProcessing/IProvider.php',
'OCP\\TaskProcessing\\ISynchronousProvider' => $baseDir . '/lib/public/TaskProcessing/ISynchronousProvider.php',
'OCP\\TaskProcessing\\ITaskType' => $baseDir . '/lib/public/TaskProcessing/ITaskType.php',
'OCP\\TaskProcessing\\ITriggerableProvider' => $baseDir . '/lib/public/TaskProcessing/ITriggerableProvider.php',
'OCP\\TaskProcessing\\ShapeDescriptor' => $baseDir . '/lib/public/TaskProcessing/ShapeDescriptor.php',
'OCP\\TaskProcessing\\ShapeEnumValue' => $baseDir . '/lib/public/TaskProcessing/ShapeEnumValue.php',
'OCP\\TaskProcessing\\Task' => $baseDir . '/lib/public/TaskProcessing/Task.php',
Expand Down
13 changes: 7 additions & 6 deletions lib/composer/composer/autoload_static.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,32 +11,32 @@ class ComposerStaticInit749170dad3f5e7f9ca158f5a9f04f6a2
);

public static $prefixLengthsPsr4 = array (
'O' =>
'O' =>
array (
'OC\\Core\\' => 8,
'OC\\' => 3,
'OCP\\' => 4,
),
'N' =>
'N' =>
array (
'NCU\\' => 4,
),
);

public static $prefixDirsPsr4 = array (
'OC\\Core\\' =>
'OC\\Core\\' =>
array (
0 => __DIR__ . '/../../..' . '/core',
),
'OC\\' =>
'OC\\' =>
array (
0 => __DIR__ . '/../../..' . '/lib/private',
),
'OCP\\' =>
'OCP\\' =>
array (
0 => __DIR__ . '/../../..' . '/lib/public',
),
'NCU\\' =>
'NCU\\' =>
array (
0 => __DIR__ . '/../../..' . '/lib/unstable',
),
Expand Down Expand Up @@ -913,6 +913,7 @@ class ComposerStaticInit749170dad3f5e7f9ca158f5a9f04f6a2
'OCP\\TaskProcessing\\IProvider' => __DIR__ . '/../../..' . '/lib/public/TaskProcessing/IProvider.php',
'OCP\\TaskProcessing\\ISynchronousProvider' => __DIR__ . '/../../..' . '/lib/public/TaskProcessing/ISynchronousProvider.php',
'OCP\\TaskProcessing\\ITaskType' => __DIR__ . '/../../..' . '/lib/public/TaskProcessing/ITaskType.php',
'OCP\\TaskProcessing\\ITriggerableProvider' => __DIR__ . '/../../..' . '/lib/public/TaskProcessing/ITriggerableProvider.php',
'OCP\\TaskProcessing\\ShapeDescriptor' => __DIR__ . '/../../..' . '/lib/public/TaskProcessing/ShapeDescriptor.php',
'OCP\\TaskProcessing\\ShapeEnumValue' => __DIR__ . '/../../..' . '/lib/public/TaskProcessing/ShapeEnumValue.php',
'OCP\\TaskProcessing\\Task' => __DIR__ . '/../../..' . '/lib/public/TaskProcessing/Task.php',
Expand Down
16 changes: 16 additions & 0 deletions lib/private/TaskProcessing/Db/TaskMapper.php
Original file line number Diff line number Diff line change
Expand Up @@ -264,4 +264,20 @@ public function findNOldestScheduledByType(array $taskTypes, array $taskIdsToIgn

return $this->findEntities($qb);
}

/**
* @throws Exception
*/
public function hasRunningTasksForTaskType(string $getTaskTypeId): bool {
$qb = $this->db->getQueryBuilder();
$qb->select('id')
->from($this->tableName);
$qb->where($qb->expr()->eq('type', $qb->createNamedParameter($getTaskTypeId)));
$qb->andWhere($qb->expr()->eq('status', $qb->createNamedParameter(\OCP\TaskProcessing\Task::STATUS_RUNNING, IQueryBuilder::PARAM_INT)));
$qb->setMaxResults(1);
$result = $qb->executeQuery();
$hasRunningTasks = $result->fetch() !== false;
$result->closeCursor();
return $hasRunningTasks;
}
}
23 changes: 23 additions & 0 deletions lib/private/TaskProcessing/Manager.php
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@
use OCP\TaskProcessing\IProvider;
use OCP\TaskProcessing\ISynchronousProvider;
use OCP\TaskProcessing\ITaskType;
use OCP\TaskProcessing\ITriggerableProvider;
use OCP\TaskProcessing\ShapeDescriptor;
use OCP\TaskProcessing\ShapeEnumValue;
use OCP\TaskProcessing\Task;
Expand Down Expand Up @@ -976,6 +977,28 @@ public function scheduleTask(Task $task): void {
if ($provider instanceof ISynchronousProvider) {
$this->jobList->add(SynchronousBackgroundJob::class, null);
}
if ($provider instanceof ITriggerableProvider) {
try {
if (!$this->taskMapper->hasRunningTasksForTaskType($task->getTaskTypeId())) {
// If no tasks are currently running for this task type, nudge the provider to ask for tasks
try {
$provider->trigger();
} catch (\Throwable $e) {
$this->logger->error('Failed to trigger the provider after scheduling a task.', [
'exception' => $e,
'taskId' => $task->getId(),
'providerId' => $provider->getId(),
]);
}
}
} catch (Exception $e) {
$this->logger->error('Failed to check DB for running tasks after a task was scheduled for a triggerable provider. Not triggering the provider.', [
'exception' => $e,
'taskId' => $task->getId(),
'providerId' => $provider->getId()
]);
}
}
}

public function runTask(Task $task): Task {
Expand Down
27 changes: 27 additions & 0 deletions lib/public/TaskProcessing/ITriggerableProvider.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<?php

declare(strict_types=1);

/**
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/


namespace OCP\TaskProcessing;

/**
* This is the interface that is implemented by apps that
* implement a task processing provider with support for being triggered
* @since 33.0.0
*/
interface ITriggerableProvider extends IProvider {

/**
* Called when new tasks for this provider are coming in and there are currently
* no tasks running for this provider's task type
*
* @since 33.0.0
*/
public function trigger(): void;
}
87 changes: 87 additions & 0 deletions tests/lib/TaskProcessing/TaskProcessingTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@
use OCP\TaskProcessing\IProvider;
use OCP\TaskProcessing\ISynchronousProvider;
use OCP\TaskProcessing\ITaskType;
use OCP\TaskProcessing\ITriggerableProvider;
use OCP\TaskProcessing\ShapeDescriptor;
use OCP\TaskProcessing\Task;
use OCP\TaskProcessing\TaskTypes\TextToImage;
Expand Down Expand Up @@ -438,6 +439,53 @@ public function getOptionalOutputShapeEnumValues(): array {
}
}


class ExternalTriggerableProvider implements ITriggerableProvider {
public const ID = 'event:external:provider:triggerable';
public const TASK_TYPE_ID = TextToText::ID;

public function getId(): string {
return self::ID;
}
public function getName(): string {
return 'External Triggerable Provider via Event';
}

public function getTaskTypeId(): string {
return self::TASK_TYPE_ID;
}

public function trigger(): void {
}
public function getExpectedRuntime(): int {
return 5;
}
public function getOptionalInputShape(): array {
return [];
}
public function getOptionalOutputShape(): array {
return [];
}
public function getInputShapeEnumValues(): array {
return [];
}
public function getInputShapeDefaults(): array {
return [];
}
public function getOptionalInputShapeEnumValues(): array {
return [];
}
public function getOptionalInputShapeDefaults(): array {
return [];
}
public function getOutputShapeEnumValues(): array {
return [];
}
public function getOptionalOutputShapeEnumValues(): array {
return [];
}
}

class ConflictingExternalProvider implements IProvider {
// Same ID as SuccessfulSyncProvider
public const ID = 'test:sync:success';
Expand Down Expand Up @@ -555,6 +603,7 @@ protected function setUp(): void {
SuccessfulTextToImageProvider::class => new SuccessfulTextToImageProvider(),
FailingTextToImageProvider::class => new FailingTextToImageProvider(),
ExternalProvider::class => new ExternalProvider(),
ExternalTriggerableProvider::class => new ExternalTriggerableProvider(),
ConflictingExternalProvider::class => new ConflictingExternalProvider(),
ExternalTaskType::class => new ExternalTaskType(),
ConflictingExternalTaskType::class => new ConflictingExternalTaskType(),
Expand Down Expand Up @@ -1227,6 +1276,44 @@ public function testLocalProviderWinsConflictWithEvent() {
self::assertCount(1, $providers); // Ensure no extra provider was added
}

public function testTriggerableProviderWithNoOtherRunningTasks() {
// Arrange: Local provider registered, conflicting external provider via event
$this->registrationContext->expects($this->any())->method('getTaskProcessingProviders')->willReturn([]);
$this->registrationContext->expects($this->any())->method('getTextProcessingProviders')->willReturn([]);
$this->registrationContext->expects($this->any())->method('getTextToImageProviders')->willReturn([]);
$this->registrationContext->expects($this->any())->method('getSpeechToTextProviders')->willReturn([]);

$externalProvider = $this->createPartialMock(ExternalTriggerableProvider::class, ['trigger']);
$externalProvider->expects($this->once())->method('trigger');
$this->configureEventDispatcherMock(providersToAdd: [$externalProvider]);
$this->manager = $this->createManagerInstance();

// Act
$task = new Task($externalProvider->getTaskTypeId(), ['input' => ''], 'tests', 'foobar');
$this->manager->scheduleTask($task);
}

public function testTriggerableProviderWithOtherRunningTasks() {
// Arrange: Local provider registered, conflicting external provider via event
$this->registrationContext->expects($this->any())->method('getTaskProcessingProviders')->willReturn([]);
$this->registrationContext->expects($this->any())->method('getTextProcessingProviders')->willReturn([]);
$this->registrationContext->expects($this->any())->method('getTextToImageProviders')->willReturn([]);
$this->registrationContext->expects($this->any())->method('getSpeechToTextProviders')->willReturn([]);

$externalProvider = $this->createPartialMock(ExternalTriggerableProvider::class, ['trigger']);
$externalProvider->expects($this->once())->method('trigger');
$this->configureEventDispatcherMock(providersToAdd: [$externalProvider]);
$this->manager = $this->createManagerInstance();

$task = new Task($externalProvider->getTaskTypeId(), ['input' => ''], 'tests', 'foobar');
$this->manager->scheduleTask($task);
$this->manager->lockTask($task);

// Act
$task = new Task($externalProvider->getTaskTypeId(), ['input' => ''], 'tests', 'foobar');
$this->manager->scheduleTask($task);
}

public function testMergeTaskTypesLocalAndEvent() {
// Arrange: Local type registered, DIFFERENT external type via event
$this->registrationContext->expects($this->any())->method('getTaskProcessingProviders')->willReturn([
Expand Down
Loading