feat(TaskProcessing): Introduce ITriggerableProvider

Signed-off-by: Marcel Klehr <mklehr@gmx.net>

# Conflicts:
#	lib/private/TaskProcessing/Db/TaskMapper.php
pull/55741/head
Marcel Klehr 3 months ago
parent 4c17229789
commit 044e92bf4e
  1. 1
      lib/composer/composer/autoload_classmap.php
  2. 13
      lib/composer/composer/autoload_static.php
  3. 16
      lib/private/TaskProcessing/Db/TaskMapper.php
  4. 11
      lib/private/TaskProcessing/Manager.php
  5. 27
      lib/public/TaskProcessing/ITriggerableProvider.php
  6. 87
      tests/lib/TaskProcessing/TaskProcessingTest.php

@ -872,6 +872,7 @@ return array(
'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',

@ -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',
),
@ -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',

@ -264,4 +264,20 @@ class TaskMapper extends QBMapper {
return $this->findEntities($qb);
}
/**
* @throws Exception
*/
public function hasRunningTasksForTaskType(string $getTaskTypeId): bool {
$qb = $this->db->getQueryBuilder();
$qb->select(Task::$columns)
->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;
}
}

@ -56,6 +56,7 @@ use OCP\TaskProcessing\IManager;
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;
@ -976,6 +977,16 @@ class Manager implements IManager {
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
$provider->trigger();
}
} 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]);
}
}
}
public function runTask(Task $task): Task {

@ -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;
}

@ -47,6 +47,7 @@ use OCP\TaskProcessing\IManager;
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;
@ -438,6 +439,53 @@ class ExternalProvider implements IProvider {
}
}
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';
@ -555,6 +603,7 @@ class TaskProcessingTest extends \Test\TestCase {
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(),
@ -1227,6 +1276,44 @@ class TaskProcessingTest extends \Test\TestCase {
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([

Loading…
Cancel
Save