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
2 changes: 1 addition & 1 deletion .php-cs-fixer.dist.php
Original file line number Diff line number Diff line change
Expand Up @@ -42,5 +42,5 @@
'BlitzPHP Tasks',
'Dimitri Sitchet Tomkeu',
'devcode.dst@gmail.com',
2025
2025,
);
2 changes: 1 addition & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
},
"require-dev": {
"blitz-php/coding-standard": "^1.4",
"blitz-php/framework": "^0.11.3",
"blitz-php/framework": "^0.12",
"blitz-php/http-client": "^0.0.2",
"kahlan/kahlan": "^6.0",
"phpstan/phpstan": "^2.1"
Expand Down
134 changes: 132 additions & 2 deletions spec/Task.spec.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@

use BlitzPHP\Spec\CliOutputHelper as COH;
use BlitzPHP\Spec\ReflectionHelper;
use BlitzPHP\Tasks\Exceptions\TasksException;
use BlitzPHP\Tasks\Task;
use BlitzPHP\Utilities\Date;
use BlitzPHP\Utilities\Helpers;
Expand Down Expand Up @@ -82,7 +83,7 @@
$task->run();

expect(COH::buffer())->toMatch(
static fn ($actual) => str_contains($actual, 'La commande peut produire du texte.')
static fn ($actual) => str_contains($actual, 'La commande peut produire du texte.'),
);
});

Expand Down Expand Up @@ -137,7 +138,136 @@
expect($task->lastRun()->format('Y-m-d H:i:s'))->toBe($date);
});

it('Peut executer une commande shell', function () {
it('Execute une tache unique', function () {
$task = new Task('command', 'foo:bar');

expect($task->singleInstance)->toBeFalsy();

$result = $task->singleInstance();
expect($task->singleInstance)->toBeTruthy();
expect($task->singleInstanceTTL)->toBeNull();
expect($result)->toBe($task);

// Test avec un TTL personnalisé
$task->singleInstance(3600);
expect($task->singleInstance)->toBeTruthy();
expect($task->singleInstanceTTL)->toBe(3600);
});

it('Recuperation de la cle de verouillage', function () {
$task = new Task('command', 'foo:bar');
$task->named('test_task');

$method = ReflectionHelper::getPrivateMethodInvoker($task, 'getLockKey');
$expected = 'task_lock_test_task';

expect($method())->toBe($expected);

// Test avec une tache sans nom - doit utiliser le nom dynamique
$task = new Task('command', 'foo:bar');
$method = ReflectionHelper::getPrivateMethodInvoker($task, 'getLockKey');

// Doit utiliser le nom de la tache depuis le getter magique
$expected = "task_lock_{$task->name}";
expect($method())->toBe($expected);
});

it('Cohérence de la clé de verrouillage pour les tâches nommées', function () {
// Créer deux tâches closure différentes avec le même nom
$closure1 = static fn () => 'test1';

$closure2 = static function () {
return 'test2'; // Different functionality
};

$task1 = new Task('closure', $closure1);
$task2 = new Task('closure', $closure2);

// Si elles ont le même nom, elles devraient avoir la même clé de verrouillage
$task1->named('same_name');
$task2->named('same_name');

$getLockKey1 = ReflectionHelper::getPrivateMethodInvoker($task1, 'getLockKey');
$getLockKey2 = ReflectionHelper::getPrivateMethodInvoker($task2, 'getLockKey');

expect($getLockKey1())->toBe($getLockKey2());

// Des noms différents devraient produire des clés différentes
$task3 = new Task('closure', $closure1);
$task3->named('different_name');

$getLockKey3 = ReflectionHelper::getPrivateMethodInvoker($task3, 'getLockKey');

expect($getLockKey1())->not->toBe($getLockKey3());
});

it('shouldRun avec singleInstance', function () {
$task = (new Task('command', 'foo:bar'))
->named('test_should_run')
->hourly()
->singleInstance();

// Devrait s'exécuter à l'heure correcte sans verrou existant
expect($task->shouldRun('12:00am'))->toBeTruthy();

// Créer un verrou
$lockKey = ReflectionHelper::getPrivateMethodInvoker($task, 'getLockKey')();
cache()->set($lockKey, [], 3600);

// Should not run if a lock exists
expect($task->shouldRun('12:00am'))->toBeFalsy();

cache()->delete($lockKey);
});

it('run avec singleInstance', function () {
$task = new Task('closure', static fn () => 'task executed');
$task->named('test_run_single');
$task->singleInstance();

$result = $task->run();
expect($result)->toBe('task executed');
$lockKey = ReflectionHelper::getPrivateMethodInvoker($task, 'getLockKey')();
expect(cache()->get($lockKey))->toBeNull();
});

it('verrou libéré après une exception', function () {
$task = new Task('command', 'invalid:command');
$task->named('test_exception');
$task->singleInstance();

$reflection = new ReflectionClass($task);
$property = $reflection->getProperty('type');
$property->setValue($task, 'invalid_type');

$lockKey = ReflectionHelper::getPrivateMethodInvoker($task, 'getLockKey')();

try {
$task->run();
expect(false)->toBe(true); // Should not reach here
} catch (Exception $e) {
expect($e)->toBeAnInstanceOf(TasksException::class);
}

expect(cache()->get($lockKey))->toBeNull();
});

it('singleInstance avec TTL personnalisé', function () {
$task = new Task('closure', static fn () => 'done');
$task->named('test_ttl');

$task->singleInstance(60);

expect(ReflectionHelper::getPrivateProperty($task, 'singleInstanceTTL'))->toBe(60);

$task2 = new Task('closure', static fn () => 'done');
$task2->named('test_no_ttl');
$task2->singleInstance();

expect(ReflectionHelper::getPrivateProperty($task2, 'singleInstanceTTL'))->toBeNull();
});

it('Peut exécuter une commande shell', function () {
expect(file_exists($path = __DIR__ . '/test.php'))->toBeFalsy();

$task = new Task('shell', 'cp ' . __FILE__ . ' ' . $path);
Expand Down
1 change: 1 addition & 0 deletions spec/bootstrap.php
Original file line number Diff line number Diff line change
Expand Up @@ -30,3 +30,4 @@
config()->load('parametres', VENDOR_PATH . 'blitz-php/parametres/src/Config/parametres.php');
config()->set('parametres.file.path', storage_path('.parametres.json'));
config()->load('tasks', __DIR__ . '/../src/Config/tasks.php');
config()->ghost('cache', ['handler' => 'file']);
2 changes: 1 addition & 1 deletion src/Commands/Work.php
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ public function execute(array $params)
'%s %s %s',
escapeshellarg(PHP_BINARY),
escapeshellarg(base_path('klinge')),
'tasks:run'
'tasks:run',
);

if ($this->option('run-output-file')) {
Expand Down
59 changes: 55 additions & 4 deletions src/Task.php
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,16 @@ class Task
*/
protected array $environments = [];

/**
* Faut-il empêcher les exécutions simultanées de cette tâche ?
*/
protected bool $singleInstance = false;

/**
* Durée maximale de verrouillage en secondes pour les tâches single instance.
*/
protected ?int $singleInstanceTTL = null;

/**
* Timezone dans lequel la tâche doit être traitée.
*/
Expand Down Expand Up @@ -122,12 +132,23 @@ public function getAction(): mixed
*/
public function run()
{
$method = 'run' . ucfirst($this->type);
if (! method_exists($this, $method)) {
throw TasksException::invalidTaskType($this->type);
if ($this->singleInstance) {
$lockKey = $this->getLockKey();
cache()->set($lockKey, [], $this->singleInstanceTTL ?? 0);
}

return $this->process($this->container, $method);
try {
$method = 'run' . ucfirst($this->type);
if (! method_exists($this, $method)) {
throw TasksException::invalidTaskType($this->type);
}

return $this->process($this->container, $method);
} finally {
if ($this->singleInstance) {
cache()->delete($lockKey);
}
}
}

/**
Expand All @@ -147,9 +168,27 @@ public function shouldRun(?string $testTime = null): bool
return false;
}

// S'il s'agit d'une tâche à instance unique et qu'un verrou est présent, ne pas l'exécuter
if ($this->singleInstance && cache()->get($this->getLockKey()) !== null) {
return false;
}

return $cron->shouldRun($this->getExpression());
}

/**
* Définit cette tâche comme une instance unique
*
* @param int|null $lockTTL Durée de vie du verrou de cache en secondes
*/
public function singleInstance(?int $lockTTL = null): static
{
$this->singleInstance = true;
$this->singleInstanceTTL = $lockTTL;

return $this;
}

/**
* Limite l'exécution de cette tâche uniquement dans des environnements spécifiés.
*/
Expand Down Expand Up @@ -322,4 +361,16 @@ public function __set(string $name, mixed $value): void
{
$this->attributes[$name] = $value;
}

/**
* Détermine la clé de verrouillage pour la tâche.
*
* @throws ReflectionException
*/
private function getLockKey(): string
{
$name = $this->attributes['name'] ?? $this->buildName();

return 'task_lock_' . $name;
}
}