6 - Wire PollFailed dispatch and listener
This commit is contained in:
parent
6ab175a466
commit
31a53de9fb
6 changed files with 177 additions and 10 deletions
18
app/Listeners/PollFailedListener.php
Normal file
18
app/Listeners/PollFailedListener.php
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Listeners;
|
||||||
|
|
||||||
|
use App\Services\PollAlertService;
|
||||||
|
use Lvl0\FediDiscover\Events\PollFailed;
|
||||||
|
|
||||||
|
class PollFailedListener
|
||||||
|
{
|
||||||
|
public function __construct(private PollAlertService $service) {}
|
||||||
|
|
||||||
|
public function handle(PollFailed $event): void
|
||||||
|
{
|
||||||
|
$this->service->recordFailure($event->instance, $event->message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -2,10 +2,12 @@
|
||||||
|
|
||||||
namespace App\Providers;
|
namespace App\Providers;
|
||||||
|
|
||||||
|
use App\Listeners\PollFailedListener;
|
||||||
use App\Listeners\UrlDiscoveredListener;
|
use App\Listeners\UrlDiscoveredListener;
|
||||||
use App\Services\LanguageDetectionService;
|
use App\Services\LanguageDetectionService;
|
||||||
use Illuminate\Support\Facades\Event;
|
use Illuminate\Support\Facades\Event;
|
||||||
use Illuminate\Support\ServiceProvider;
|
use Illuminate\Support\ServiceProvider;
|
||||||
|
use Lvl0\FediDiscover\Events\PollFailed;
|
||||||
use Lvl0\FediDiscover\Events\UrlDiscovered;
|
use Lvl0\FediDiscover\Events\UrlDiscovered;
|
||||||
|
|
||||||
class AppServiceProvider extends ServiceProvider
|
class AppServiceProvider extends ServiceProvider
|
||||||
|
|
@ -18,5 +20,6 @@ public function register(): void
|
||||||
public function boot(): void
|
public function boot(): void
|
||||||
{
|
{
|
||||||
Event::listen(UrlDiscovered::class, UrlDiscoveredListener::class);
|
Event::listen(UrlDiscovered::class, UrlDiscoveredListener::class);
|
||||||
|
Event::listen(PollFailed::class, PollFailedListener::class);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@
|
||||||
use Illuminate\Console\Command;
|
use Illuminate\Console\Command;
|
||||||
use Illuminate\Support\Facades\Log;
|
use Illuminate\Support\Facades\Log;
|
||||||
use Lvl0\FediDiscover\Actions\PollFediverseAction;
|
use Lvl0\FediDiscover\Actions\PollFediverseAction;
|
||||||
|
use Lvl0\FediDiscover\Events\PollFailed;
|
||||||
use Lvl0\FediDiscover\Models\Instance;
|
use Lvl0\FediDiscover\Models\Instance;
|
||||||
use Throwable;
|
use Throwable;
|
||||||
|
|
||||||
|
|
@ -24,13 +25,13 @@ public function __construct(
|
||||||
|
|
||||||
public function handle(): int
|
public function handle(): int
|
||||||
{
|
{
|
||||||
$hadFailure = false;
|
$errors = Instance::enabled()
|
||||||
|
|
||||||
Instance::enabled()
|
|
||||||
->get()
|
->get()
|
||||||
->each(function (Instance $instance) use (&$hadFailure) {
|
->map(function (Instance $instance) {
|
||||||
try {
|
try {
|
||||||
$this->action->execute($instance);
|
$this->action->execute($instance);
|
||||||
|
|
||||||
|
return ['instance_id' => $instance->id, 'status' => 'success'];
|
||||||
} catch (Throwable $e) {
|
} catch (Throwable $e) {
|
||||||
$this->error("Failed to poll {$instance->url}: {$e->getMessage()}");
|
$this->error("Failed to poll {$instance->url}: {$e->getMessage()}");
|
||||||
Log::warning('fedi-discover:poll failed', [
|
Log::warning('fedi-discover:poll failed', [
|
||||||
|
|
@ -39,14 +40,22 @@ public function handle(): int
|
||||||
'exception' => $e::class,
|
'exception' => $e::class,
|
||||||
'message' => $e->getMessage(),
|
'message' => $e->getMessage(),
|
||||||
]);
|
]);
|
||||||
$hadFailure = true;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if ($hadFailure) {
|
return ['instance' => $instance, 'status' => 'error', 'error' => $e->getMessage()];
|
||||||
return self::FAILURE;
|
}
|
||||||
|
})
|
||||||
|
->filter(fn (array $res) => $res['status'] === 'error');
|
||||||
|
|
||||||
|
if ($errors->isEmpty()) {
|
||||||
|
return self::SUCCESS;
|
||||||
}
|
}
|
||||||
|
|
||||||
return self::SUCCESS;
|
$errors->each(fn (array $errorArr) => PollFailed::dispatch(
|
||||||
|
$errorArr['instance'],
|
||||||
|
$errorArr['error'] ?? '',
|
||||||
|
now()->toImmutable(),
|
||||||
|
));
|
||||||
|
|
||||||
|
return self::FAILURE;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,10 +5,12 @@
|
||||||
namespace Lvl0\FediDiscover\Tests\Feature;
|
namespace Lvl0\FediDiscover\Tests\Feature;
|
||||||
|
|
||||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||||
|
use Illuminate\Support\Facades\Event;
|
||||||
use Lvl0\FediDiscover\Actions\PollFediverseAction;
|
use Lvl0\FediDiscover\Actions\PollFediverseAction;
|
||||||
use Lvl0\FediDiscover\Clients\FediverseClientFactory;
|
use Lvl0\FediDiscover\Clients\FediverseClientFactory;
|
||||||
use Lvl0\FediDiscover\Clients\FediverseClientInterface;
|
use Lvl0\FediDiscover\Clients\FediverseClientInterface;
|
||||||
use Lvl0\FediDiscover\Config\InstanceType;
|
use Lvl0\FediDiscover\Config\InstanceType;
|
||||||
|
use Lvl0\FediDiscover\Events\PollFailed;
|
||||||
use Lvl0\FediDiscover\Models\Instance;
|
use Lvl0\FediDiscover\Models\Instance;
|
||||||
use Mockery;
|
use Mockery;
|
||||||
use RuntimeException;
|
use RuntimeException;
|
||||||
|
|
@ -122,6 +124,52 @@ public function test_one_instance_throwing_does_not_stop_remaining_instances_fro
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function test_poll_failed_event_is_dispatched_when_action_throws(): void
|
||||||
|
{
|
||||||
|
Event::fake([PollFailed::class]);
|
||||||
|
|
||||||
|
$instance = Instance::create([
|
||||||
|
'type' => InstanceType::Mastodon,
|
||||||
|
'url' => 'https://failing.example',
|
||||||
|
'enabled' => true,
|
||||||
|
'interval_seconds' => 600,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$action = Mockery::mock(PollFediverseAction::class);
|
||||||
|
$action->shouldReceive('execute')
|
||||||
|
->once()
|
||||||
|
->andReturnUsing(function (): void {
|
||||||
|
throw new RuntimeException('Connection refused');
|
||||||
|
});
|
||||||
|
|
||||||
|
$this->app->instance(PollFediverseAction::class, $action);
|
||||||
|
|
||||||
|
$this->artisan('fedi-discover:poll');
|
||||||
|
|
||||||
|
Event::assertDispatched(PollFailed::class, function (PollFailed $event) use ($instance): bool {
|
||||||
|
return $event->instance->id === $instance->id
|
||||||
|
&& $event->message === 'Connection refused';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_poll_failed_event_is_not_dispatched_on_a_successful_poll(): void
|
||||||
|
{
|
||||||
|
Event::fake([PollFailed::class]);
|
||||||
|
|
||||||
|
Instance::create([
|
||||||
|
'type' => InstanceType::Mastodon,
|
||||||
|
'url' => 'https://healthy.example',
|
||||||
|
'enabled' => true,
|
||||||
|
'interval_seconds' => 600,
|
||||||
|
]);
|
||||||
|
|
||||||
|
// setUp() already binds a no-op action stub via the factory; no override needed.
|
||||||
|
|
||||||
|
$this->artisan('fedi-discover:poll');
|
||||||
|
|
||||||
|
Event::assertNotDispatched(PollFailed::class);
|
||||||
|
}
|
||||||
|
|
||||||
public function test_it_exits_one_when_at_least_one_instance_fails(): void
|
public function test_it_exits_one_when_at_least_one_instance_fails(): void
|
||||||
{
|
{
|
||||||
Instance::create([
|
Instance::create([
|
||||||
|
|
|
||||||
52
tests/Feature/Listeners/PollFailedListenerTest.php
Normal file
52
tests/Feature/Listeners/PollFailedListenerTest.php
Normal file
|
|
@ -0,0 +1,52 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Tests\Feature\Listeners;
|
||||||
|
|
||||||
|
use App\Listeners\PollFailedListener;
|
||||||
|
use App\Services\PollAlertService;
|
||||||
|
use Carbon\CarbonImmutable;
|
||||||
|
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||||
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||||
|
use Lvl0\FediDiscover\Config\InstanceType;
|
||||||
|
use Lvl0\FediDiscover\Events\PollFailed;
|
||||||
|
use Lvl0\FediDiscover\Models\Instance;
|
||||||
|
use Mockery;
|
||||||
|
use Tests\TestCase;
|
||||||
|
|
||||||
|
class PollFailedListenerTest extends TestCase
|
||||||
|
{
|
||||||
|
use RefreshDatabase;
|
||||||
|
|
||||||
|
public function test_handle_calls_record_failure_with_the_event_instance_and_message(): void
|
||||||
|
{
|
||||||
|
$instance = Instance::factory()
|
||||||
|
->type(InstanceType::Mastodon)
|
||||||
|
->enabled()
|
||||||
|
->create(['consecutive_poll_failures' => 0]);
|
||||||
|
|
||||||
|
$message = 'connection timed out';
|
||||||
|
$failedAt = CarbonImmutable::now();
|
||||||
|
$event = new PollFailed($instance, $message, $failedAt);
|
||||||
|
|
||||||
|
$service = Mockery::mock(PollAlertService::class);
|
||||||
|
$service->shouldReceive('recordFailure')
|
||||||
|
->once()
|
||||||
|
->with(
|
||||||
|
Mockery::on(fn (Instance $i) => $i->is($instance)),
|
||||||
|
$message,
|
||||||
|
);
|
||||||
|
|
||||||
|
$listener = new PollFailedListener($service);
|
||||||
|
$listener->handle($event);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_listener_is_not_queued(): void
|
||||||
|
{
|
||||||
|
$this->assertNotInstanceOf(
|
||||||
|
ShouldQueue::class,
|
||||||
|
new PollFailedListener($this->createStub(PollAlertService::class)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
37
tests/Feature/PollFailedIntegrationTest.php
Normal file
37
tests/Feature/PollFailedIntegrationTest.php
Normal file
|
|
@ -0,0 +1,37 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Tests\Feature;
|
||||||
|
|
||||||
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||||
|
use Illuminate\Support\Facades\Http;
|
||||||
|
use Lvl0\FediDiscover\Actions\PollFediverseAction;
|
||||||
|
use Lvl0\FediDiscover\Config\InstanceType;
|
||||||
|
use Lvl0\FediDiscover\Models\Instance;
|
||||||
|
use RuntimeException;
|
||||||
|
use Tests\TestCase;
|
||||||
|
|
||||||
|
class PollFailedIntegrationTest extends TestCase
|
||||||
|
{
|
||||||
|
use RefreshDatabase;
|
||||||
|
|
||||||
|
public function test_poll_failure_increments_consecutive_poll_failures_via_full_chain(): void
|
||||||
|
{
|
||||||
|
Http::fake();
|
||||||
|
|
||||||
|
$instance = Instance::factory()
|
||||||
|
->type(InstanceType::Mastodon)
|
||||||
|
->enabled()
|
||||||
|
->create(['consecutive_poll_failures' => 0]);
|
||||||
|
|
||||||
|
$this->mock(PollFediverseAction::class)
|
||||||
|
->shouldReceive('execute')
|
||||||
|
->once()
|
||||||
|
->andThrow(new RuntimeException('connection refused'));
|
||||||
|
|
||||||
|
$this->artisan('fedi-discover:poll');
|
||||||
|
|
||||||
|
$this->assertSame(1, $instance->fresh()->consecutive_poll_failures);
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue