feat(webhook): add CronTrigger scheduled action — replaces 4 Docker cron containers
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
0e038ff350
commit
7db3637985
2 changed files with 158 additions and 0 deletions
56
src/Core/Webhook/CronTrigger.php
Normal file
56
src/Core/Webhook/CronTrigger.php
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* Core PHP Framework
|
||||
*
|
||||
* Licensed under the European Union Public Licence (EUPL) v1.2.
|
||||
* See LICENSE file for details.
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Core\Webhook;
|
||||
|
||||
use Core\Actions\Action;
|
||||
use Core\Actions\Scheduled;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
|
||||
#[Scheduled(frequency: 'everyMinute', withoutOverlapping: true, runInBackground: true)]
|
||||
class CronTrigger
|
||||
{
|
||||
use Action;
|
||||
|
||||
public function handle(): void
|
||||
{
|
||||
$triggers = config('webhook.cron_triggers', []);
|
||||
|
||||
foreach ($triggers as $product => $config) {
|
||||
if (empty($config['base_url'])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$baseUrl = rtrim($config['base_url'], '/');
|
||||
$key = $config['key'] ?? '';
|
||||
$stagger = (int) ($config['stagger_seconds'] ?? 0);
|
||||
$offset = (int) ($config['offset_seconds'] ?? 0);
|
||||
|
||||
if ($offset > 0) {
|
||||
usleep($offset * 1_000_000);
|
||||
}
|
||||
|
||||
foreach ($config['endpoints'] ?? [] as $i => $endpoint) {
|
||||
if ($i > 0 && $stagger > 0) {
|
||||
usleep($stagger * 1_000_000);
|
||||
}
|
||||
|
||||
$url = $baseUrl . $endpoint . '?key=' . $key;
|
||||
|
||||
try {
|
||||
Http::timeout(30)->get($url);
|
||||
} catch (\Throwable $e) {
|
||||
logger()->warning("Cron trigger failed for {$product}{$endpoint}: {$e->getMessage()}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
102
tests/Feature/CronTriggerTest.php
Normal file
102
tests/Feature/CronTriggerTest.php
Normal file
|
|
@ -0,0 +1,102 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Core\Tests\Feature;
|
||||
|
||||
use Core\Actions\Action;
|
||||
use Core\Actions\Scheduled;
|
||||
use Core\Tests\TestCase;
|
||||
use Core\Webhook\CronTrigger;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
|
||||
class CronTriggerTest extends TestCase
|
||||
{
|
||||
public function test_has_scheduled_attribute(): void
|
||||
{
|
||||
$ref = new \ReflectionClass(CronTrigger::class);
|
||||
$attrs = $ref->getAttributes(Scheduled::class);
|
||||
|
||||
$this->assertCount(1, $attrs);
|
||||
$this->assertSame('everyMinute', $attrs[0]->newInstance()->frequency);
|
||||
}
|
||||
|
||||
public function test_uses_action_trait(): void
|
||||
{
|
||||
$this->assertTrue(
|
||||
in_array(Action::class, class_uses_recursive(CronTrigger::class), true)
|
||||
);
|
||||
}
|
||||
|
||||
public function test_hits_configured_endpoints(): void
|
||||
{
|
||||
Http::fake();
|
||||
|
||||
config(['webhook.cron_triggers' => [
|
||||
'test-product' => [
|
||||
'base_url' => 'https://example.com',
|
||||
'key' => 'secret123',
|
||||
'endpoints' => ['/cron', '/cron/reports'],
|
||||
'stagger_seconds' => 0,
|
||||
'offset_seconds' => 0,
|
||||
],
|
||||
]]);
|
||||
|
||||
CronTrigger::run();
|
||||
|
||||
Http::assertSentCount(2);
|
||||
Http::assertSent(fn ($request) => str_contains($request->url(), '/cron?key=secret123'));
|
||||
Http::assertSent(fn ($request) => str_contains($request->url(), '/cron/reports?key=secret123'));
|
||||
}
|
||||
|
||||
public function test_skips_product_with_no_base_url(): void
|
||||
{
|
||||
Http::fake();
|
||||
|
||||
config(['webhook.cron_triggers' => [
|
||||
'disabled-product' => [
|
||||
'base_url' => null,
|
||||
'key' => 'secret',
|
||||
'endpoints' => ['/cron'],
|
||||
'stagger_seconds' => 0,
|
||||
'offset_seconds' => 0,
|
||||
],
|
||||
]]);
|
||||
|
||||
CronTrigger::run();
|
||||
|
||||
Http::assertSentCount(0);
|
||||
}
|
||||
|
||||
public function test_logs_failures_gracefully(): void
|
||||
{
|
||||
Http::fake([
|
||||
'*' => Http::response('error', 500),
|
||||
]);
|
||||
|
||||
config(['webhook.cron_triggers' => [
|
||||
'failing-product' => [
|
||||
'base_url' => 'https://broken.example.com',
|
||||
'key' => 'key',
|
||||
'endpoints' => ['/cron'],
|
||||
'stagger_seconds' => 0,
|
||||
'offset_seconds' => 0,
|
||||
],
|
||||
]]);
|
||||
|
||||
// Should not throw
|
||||
CronTrigger::run();
|
||||
|
||||
Http::assertSentCount(1);
|
||||
}
|
||||
|
||||
public function test_handles_empty_config(): void
|
||||
{
|
||||
Http::fake();
|
||||
config(['webhook.cron_triggers' => []]);
|
||||
|
||||
CronTrigger::run();
|
||||
|
||||
Http::assertSentCount(0);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue