-- Aufträge, die ein Agent auf seinem System ausführt (Phase 5). -- -- Bis hierher war der Agent ein angemeldeter Beobachter: Er meldete sich an und -- gab Lebenszeichen, aber der Server konnte ihm nichts zu tun geben. Zentral -- gesteuerte Sicherungen liefen deshalb nur für Quellen, die der Control-Server -- selbst erreicht. -- -- Der Agent **holt** seine Aufträge ab; der Server drückt sie ihm nicht zu. Das -- ist keine Bequemlichkeit, sondern die einzige Wahl, die in echten Netzen -- funktioniert: Ein Agent steht hinter einer Firewall, oft hinter NAT, und ist -- vom Server aus nicht erreichbar. Die Verbindung geht immer vom Agenten aus. CREATE TABLE agent_tasks ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), -- Der ausführende Agent. agent_id UUID NOT NULL REFERENCES agents(id) ON DELETE CASCADE, -- Der Lauf, zu dem der Auftrag gehört. -- -- Über ihn findet das Ergebnis zurück in die Auftragsverwaltung. Ohne -- diesen Bezug wäre eine Agentensicherung ein Backup ohne Herkunft. job_run_id UUID NOT NULL REFERENCES backup_job_runs(id) ON DELETE CASCADE, -- Die zu sichernde Quelle. job_source_id UUID NOT NULL REFERENCES backup_job_sources(id) ON DELETE CASCADE, task_type TEXT NOT NULL DEFAULT 'backup', status TEXT NOT NULL DEFAULT 'pending', -- Der vollständige Auftrag als JSON. -- -- Er enthält Quellpfad, Repositorypfad, Backupkennung, Kette, Elternbackup -- und die Einstellungen. Als JSON und nicht als Spalten, weil der Auftrag -- ein **Vertrag** zwischen Server und Agent ist: Beide Seiten werden -- getrennt aktualisiert, und ein zusätzliches Feld darf keine Migration auf -- jedem Agenten erzwingen. task_payload JSONB NOT NULL, -- Fortschritt und Ergebnis. bytes_processed BIGINT NOT NULL DEFAULT 0, bytes_written BIGINT NOT NULL DEFAULT 0, files_processed BIGINT NOT NULL DEFAULT 0, files_skipped BIGINT NOT NULL DEFAULT 0, backup_id_in_repository TEXT, error_code TEXT, error_message TEXT, failure_class TEXT, -- Lebendmeldung des ausführenden Agenten. -- -- Stirbt der Agent mitten im Lauf, bliebe der Auftrag sonst dauerhaft auf -- 'running' stehen und blockierte den Agenten für immer — ohne dass jemand -- einen Fehler sähe. Dieselbe Überlegung wie beim Scheduler (Phase 8). heartbeat_at TIMESTAMPTZ, claimed_at TIMESTAMPTZ, started_at TIMESTAMPTZ, completed_at TIMESTAMPTZ, created_at TIMESTAMPTZ NOT NULL DEFAULT now(), CONSTRAINT agent_tasks_status_valid CHECK ( status IN ('pending', 'claimed', 'running', 'succeeded', 'partial_failure', 'failed', 'cancelled') ), CONSTRAINT agent_tasks_type_valid CHECK (task_type IN ('backup', 'restore')), -- Ein abgeschlossener Auftrag hat einen Abschlusszeitpunkt. CONSTRAINT agent_tasks_completed_when_done CHECK ( status IN ('pending', 'claimed', 'running') OR completed_at IS NOT NULL ), -- Übergangene Objekte und Erfolg schließen sich aus. -- -- Dieselbe Regel wie bei den Sicherungsläufen (Phase 8): Ein Lauf mit -- übergangenen Objekten ist niemals ein Erfolg, sondern ein Teilfehler -- (verbindliche Regel 1). Sie steht als CHECK in der Datenbank, weil sie im -- Code an jeder Stelle eingehalten werden müsste — und eine vergisst es. CONSTRAINT agent_tasks_skipped_is_not_success CHECK ( files_skipped = 0 OR status <> 'succeeded' ), -- Ein gescheiterter Auftrag nennt seine Fehlerklasse. -- -- Ohne sie kann die Ausführungsschleife nicht entscheiden, ob eine -- Wiederholung sinnvoll ist (Phase 21). CONSTRAINT agent_tasks_failure_is_classified CHECK ( status <> 'failed' OR failure_class IS NOT NULL ) ); -- Ein Agent führt höchstens einen Auftrag gleichzeitig aus. -- -- Der Teilindex ist die Durchsetzung: Zwei gleichzeitige Sicherungen desselben -- Systems konkurrierten um dieselben Dateien und dieselbe Schreibsperre des -- Repositorys. Im Code ließe sich das nur mit einer Prüfung zwischen Lesen und -- Schreiben lösen — und dazwischen passt ein zweiter Control-Server. CREATE UNIQUE INDEX agent_tasks_single_active_idx ON agent_tasks (agent_id) WHERE status IN ('claimed', 'running'); -- Die Abholung sucht den ältesten offenen Auftrag eines Agenten. CREATE INDEX agent_tasks_pending_idx ON agent_tasks (agent_id, created_at) WHERE status = 'pending'; -- Die Zuordnung eines Ergebnisses zum Lauf. CREATE INDEX agent_tasks_job_run_idx ON agent_tasks (job_run_id); -- Die Freigabe verwaister Aufträge sucht über die Lebendmeldung. CREATE INDEX agent_tasks_heartbeat_idx ON agent_tasks (heartbeat_at) WHERE status IN ('claimed', 'running');