Skip to content

Commit cd327d7

Browse files
authored
chore: remove the agent feature in favor of SSH (#365)
* chore: remove the agent feature in favor of SSH The remote-agent mode (DATABASEMENT_URL + agent:run + on-server polling agent) never landed cleanly in practice. SSH tunnels already cover the "reach a private/firewalled database" use case, so the second pathway adds maintenance cost without unique value. Removes: - agent:run command, AgentController, agent API routes & middleware - Agent/AgentJob models, factories, policy, query, payload builder - Agent CRUD Livewire pages, layout menu entry, agent_id form field - DATABASEMENT_URL branches in config/{database,cache,queue,session}.php - DTO toPayload()/fromPayload() (only consumers were the agent) The jobs:recover-stuck command keeps its backup-job recovery half (handles queue worker crashes) — only the agent-lease half is dropped. Schema cleanup is forward-only: existing migrations are left untouched (production has already run them). A new drop migration removes the agents tables and database_servers.agent_id with hasTable/hasColumn guards so fresh installs and existing prod converge on the same state. * refactor: simplify the files touched by the agent removal Light cleanup pass on the three files most affected by the previous commit: - TriggerBackupAction: drop the stale @throws ValidationException now that the method no longer dispatches to an agent or re-throws. - SetCurrentOrganization::handle: collapse the deeply-nested `if ($user instanceof User)` block into an early return — the middleware now only has one branch worth nesting for. - SaveDatabaseServerRequest: extract the duplicated $backupsEnabled resolution from rules() and withValidator() into a private helper.
1 parent 183ffb8 commit cd327d7

77 files changed

Lines changed: 170 additions & 4057 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

CLAUDE.md

Lines changed: 0 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -111,19 +111,6 @@ The Docker setup provides:
111111

112112
**Queue Worker**: The queue service automatically starts with `make start` and processes jobs from the `backups` queue. It restarts automatically on failure and respects a max of 1000 jobs before auto-restarting (prevents memory leaks).
113113

114-
## Agent Mode
115-
116-
When `DATABASEMENT_URL` is set, the app runs as a remote agent — it only executes the `agent:run` CLI command (polls an API and runs `BackupTask`). It never uses the app's own database.
117-
118-
Config files check `env('DATABASEMENT_URL')` to swap database-dependent drivers for in-memory/no-op alternatives:
119-
120-
- **Database**: `agent` connection (SQLite `:memory:`)
121-
- **Cache**: `array` driver
122-
- **Queue**: `sync` driver
123-
- **Session**: `array` driver
124-
125-
This means agent mode requires zero database configuration.
126-
127114
## Architecture
128115

129116
### Application Structure

app/Console/Commands/AgentRunCommand.php

Lines changed: 0 additions & 163 deletions
This file was deleted.

app/Console/Commands/RecoverStuckJobsCommand.php

Lines changed: 5 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
namespace App\Console\Commands;
44

55
use App\Facades\AppConfig;
6-
use App\Models\AgentJob;
76
use App\Models\BackupJob;
87
use Illuminate\Console\Command;
98
use RuntimeException;
@@ -15,71 +14,9 @@ class RecoverStuckJobsCommand extends Command
1514

1615
protected $signature = 'jobs:recover-stuck';
1716

18-
protected $description = 'Recover stuck jobs (expired agent leases and timed-out backup jobs)';
17+
protected $description = 'Recover backup jobs stuck in running/pending state beyond their timeout';
1918

2019
public function handle(): int
21-
{
22-
$agentResult = $this->recoverAgentJobs();
23-
$backupResult = $this->recoverBackupJobs();
24-
25-
if (! $agentResult && ! $backupResult) {
26-
$this->info('No stuck jobs found.');
27-
}
28-
29-
return self::SUCCESS;
30-
}
31-
32-
/**
33-
* Recover expired agent job leases (reset or fail stale jobs).
34-
*/
35-
private function recoverAgentJobs(): bool
36-
{
37-
$expiredJobs = AgentJob::query()
38-
->with(['snapshot.job'])
39-
->whereIn('status', [AgentJob::STATUS_CLAIMED, AgentJob::STATUS_RUNNING])
40-
->where('lease_expires_at', '<', now())
41-
->get();
42-
43-
if ($expiredJobs->isEmpty()) {
44-
return false;
45-
}
46-
47-
$resetCount = 0;
48-
$failedCount = 0;
49-
50-
foreach ($expiredJobs as $job) {
51-
if ($job->attempts < $job->max_attempts) {
52-
$job->update([
53-
'status' => AgentJob::STATUS_PENDING,
54-
'agent_id' => null,
55-
'lease_expires_at' => null,
56-
]);
57-
$resetCount++;
58-
} else {
59-
$errorMessage = "Max attempts ({$job->max_attempts}) exceeded with expired lease.";
60-
$job->markFailed($errorMessage);
61-
62-
$job->snapshot->job->markFailed(
63-
new RuntimeException("Agent job failed: {$errorMessage}")
64-
);
65-
$failedCount++;
66-
}
67-
}
68-
69-
$this->info("Agent jobs: recovered {$resetCount}, failed {$failedCount}.");
70-
71-
return true;
72-
}
73-
74-
/**
75-
* Recover backup jobs stuck in running/pending state beyond their timeout.
76-
*
77-
* Running jobs are compared against started_at, while pending jobs (which
78-
* were never picked up) are compared against created_at. A grace period is
79-
* added on top of the configured timeout to avoid killing jobs that are
80-
* still legitimately processing.
81-
*/
82-
private function recoverBackupJobs(): bool
8320
{
8421
$timeout = AppConfig::get('backup.job_timeout') + self::GRACE_PERIOD_SECONDS;
8522
$cutoff = now()->subSeconds($timeout);
@@ -98,7 +35,9 @@ private function recoverBackupJobs(): bool
9835
->get();
9936

10037
if ($stuckJobs->isEmpty()) {
101-
return false;
38+
$this->info('No stuck jobs found.');
39+
40+
return self::SUCCESS;
10241
}
10342

10443
foreach ($stuckJobs as $job) {
@@ -109,6 +48,6 @@ private function recoverBackupJobs(): bool
10948

11049
$this->info("Backup jobs: failed {$stuckJobs->count()} stuck job(s).");
11150

112-
return true;
51+
return self::SUCCESS;
11352
}
11453
}

app/Console/Commands/RunScheduledBackups.php

Lines changed: 5 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,8 @@
33
namespace App\Console\Commands;
44

55
use App\Jobs\ProcessBackupJob;
6-
use App\Models\AgentJob;
76
use App\Models\Backup;
87
use App\Models\BackupSchedule;
9-
use App\Services\Agent\AgentJobPayloadBuilder;
108
use App\Services\Backup\BackupJobFactory;
119
use Illuminate\Console\Command;
1210
use Illuminate\Support\Facades\Log;
@@ -17,7 +15,7 @@ class RunScheduledBackups extends Command
1715

1816
protected $description = 'Run scheduled backups for a given backup schedule';
1917

20-
public function handle(BackupJobFactory $backupJobFactory, AgentJobPayloadBuilder $payloadBuilder): int
18+
public function handle(BackupJobFactory $backupJobFactory): int
2119
{
2220
$scheduleId = $this->argument('schedule');
2321

@@ -46,7 +44,7 @@ public function handle(BackupJobFactory $backupJobFactory, AgentJobPayloadBuilde
4644

4745
foreach ($backups as $backup) {
4846
try {
49-
$this->dispatch($backup, $backupJobFactory, $payloadBuilder);
47+
$this->dispatch($backup, $backupJobFactory);
5048
} catch (\Throwable $e) {
5149
$failedCount++;
5250
Log::error("Failed to dispatch backup job for server [{$backup->databaseServer->name} / {$backup->getDisplayLabel()}]", [
@@ -64,7 +62,7 @@ public function handle(BackupJobFactory $backupJobFactory, AgentJobPayloadBuilde
6462
return self::SUCCESS;
6563
}
6664

67-
private function dispatch(Backup $backup, BackupJobFactory $backupJobFactory, AgentJobPayloadBuilder $payloadBuilder): void
65+
private function dispatch(Backup $backup, BackupJobFactory $backupJobFactory): void
6866
{
6967
$server = $backup->databaseServer;
7068

@@ -73,52 +71,12 @@ private function dispatch(Backup $backup, BackupJobFactory $backupJobFactory, Ag
7371
method: 'scheduled',
7472
);
7573

76-
// Agent-backed servers with all/pattern mode return empty snapshots —
77-
// dispatch a discovery job so the agent can list databases first.
78-
if (empty($snapshots) && $server->agent_id) {
79-
$hasInflightDiscovery = AgentJob::query()
80-
->where('database_server_id', $server->id)
81-
->where('type', AgentJob::TYPE_DISCOVER)
82-
->whereIn('status', [AgentJob::STATUS_PENDING, AgentJob::STATUS_CLAIMED, AgentJob::STATUS_RUNNING])
83-
->get()
84-
->contains(fn (AgentJob $job) => ($job->payload['backup_id'] ?? null) === $backup->id);
85-
86-
if ($hasInflightDiscovery) {
87-
$this->line(" → Skipped discovery for: {$server->name} [{$backup->getDisplayLabel()}] (already in-flight)");
88-
89-
return;
90-
}
91-
92-
AgentJob::create([
93-
'type' => AgentJob::TYPE_DISCOVER,
94-
'database_server_id' => $server->id,
95-
'snapshot_id' => null,
96-
'status' => AgentJob::STATUS_PENDING,
97-
'payload' => $payloadBuilder->buildDiscovery($backup, 'scheduled', null),
98-
]);
99-
100-
$this->line(" → Dispatched discovery for: {$server->name} [{$backup->getDisplayLabel()}] via agent");
101-
102-
return;
103-
}
104-
10574
foreach ($snapshots as $snapshot) {
106-
if ($server->agent_id) {
107-
AgentJob::create([
108-
'type' => AgentJob::TYPE_BACKUP,
109-
'database_server_id' => $server->id,
110-
'snapshot_id' => $snapshot->id,
111-
'status' => AgentJob::STATUS_PENDING,
112-
'payload' => $payloadBuilder->build($snapshot),
113-
]);
114-
} else {
115-
ProcessBackupJob::dispatch($snapshot->id);
116-
}
75+
ProcessBackupJob::dispatch($snapshot->id);
11776
}
11877

11978
$count = count($snapshots);
120-
$via = $server->agent_id ? 'agent' : 'queue';
12179
$dbInfo = $count === 1 ? '1 database' : "{$count} databases";
122-
$this->line(" → Dispatched backup for: {$server->name} [{$backup->getDisplayLabel()}] ({$dbInfo}) via {$via}");
80+
$this->line(" → Dispatched backup for: {$server->name} [{$backup->getDisplayLabel()}] ({$dbInfo})");
12381
}
12482
}

0 commit comments

Comments
 (0)