DOC-16: ret-05-client-aufbewahrungsklasse-registrieren-vernichtungs-rueckruf

- dms/internal/retentionclient: HTTP-Client fuer Archive RET-05/RET-09
- dms/internal/retentiondestroy: RegisterOrRequeue (FDN-04-Requeue bei
  Fehlschlag, kein Absturz/stilles Verwerfen), DestroyCallbackHandler
  (physische Loeschung aller Datei-Revisionen ueber storage.Service,
  markiert Dokument als vernichtet, 2xx erst danach)
- dms/cmd/app: mountet destroy-callback, registriert beim Start
- dms/cmd/worker: verarbeitet Requeue-Jobs (Retry)
- nur dms_document registriert (DOC-01/FDN-02 kennen keinen separaten
  Anhang-Typ, kein Umbau angrenzender Bereiche)
- real getestet: registrierung gegen echten fake-RET-05-Server, echte
  Loeschung inkl. Storage-Byte-Nachweis, echter Requeue-Eintrag bei
  Fehlschlag, erfolgreicher Retry
- live auf 131 gegen echten RET-09-Dienst (Port 8095) verifiziert,
  echter curl-Vernichtungs-Rueckruf mit real geloeschter Datei
- reale Core-API-06-Deploy-Luecke dokumentiert (nicht verschwiegen):
  /internal/resync/usage auf 131 aktuell nicht gemountet

Pruefungen siehe dms/docs/DOC-16-PRUEFPROTOKOLL.md
This commit is contained in:
sysops
2026-08-30 09:37:12 +02:00
parent 9a374dd91e
commit 9843275e9c
6 changed files with 651 additions and 3 deletions
+64
View File
@@ -4,11 +4,19 @@
package main
import (
"context"
"log"
"net/http"
"os"
"time"
"github.com/jackc/pgx/v5/pgxpool"
"gitea.perlbach24.de/scripte/nexarch/dms/internal/jobqueue"
"gitea.perlbach24.de/scripte/nexarch/dms/internal/retentionclient"
"gitea.perlbach24.de/scripte/nexarch/dms/internal/retentiondestroy"
"gitea.perlbach24.de/scripte/nexarch/dms/internal/shared"
"gitea.perlbach24.de/scripte/nexarch/dms/internal/storage"
)
func main() {
@@ -23,8 +31,64 @@ func main() {
_, _ = w.Write([]byte(`{"status":"ok","service":"dms-app","version":"` + shared.Version + `"}`))
})
// DOC-16: RET-05-Client (Registrierung + Vernichtungs-Rückruf-Empfang).
// Optional, um cmd/app auch ohne diese Umgebungsvariablen weiter
// startbar zu halten (z. B. isolierte Tests anderer Endpunkte).
if dsn := os.Getenv("NEXARCH_DMS_APP_TENANT_DSN"); dsn != "" {
setupRetention(mux, dsn)
} else {
log.Print("dms-app: NEXARCH_DMS_APP_TENANT_DSN nicht gesetzt, RET-05-Anbindung (DOC-16) deaktiviert")
}
log.Printf("dms-app hoert auf %s (version %s)", addr, shared.Version)
if err := http.ListenAndServe(addr, mux); err != nil {
log.Fatal(err)
}
}
func setupRetention(mux *http.ServeMux, dsn string) {
ctx := context.Background()
pool, err := pgxpool.New(ctx, dsn)
if err != nil {
log.Fatalf("dms-app: datenbankverbindung: %v", err)
}
storageDir := os.Getenv("NEXARCH_DMS_APP_STORAGE_DIR")
signingSecret := os.Getenv("NEXARCH_DMS_APP_STORAGE_SIGNING_SECRET")
publicBaseURL := os.Getenv("NEXARCH_DMS_APP_STORAGE_PUBLIC_BASE_URL")
driver := storage.NewLocalDriver(storageDir, []byte(signingSecret), publicBaseURL)
usageEndpoint := os.Getenv("NEXARCH_DMS_APP_USAGE_ENDPOINT_URL")
usageClientID := os.Getenv("NEXARCH_DMS_APP_USAGE_CLIENT_ID")
usageClientSecret := os.Getenv("NEXARCH_DMS_APP_USAGE_CLIENT_SECRET")
tenantSlug := os.Getenv("NEXARCH_DMS_APP_TENANT_SLUG")
usageReporter := storage.NewHTTPUsageReporter(usageEndpoint, usageClientID, usageClientSecret, nil)
storageSvc := storage.NewService(driver, usageReporter, tenantSlug)
mux.HandleFunc("POST /retention/destroy-callback", retentiondestroy.DestroyCallbackHandler(pool, storageSvc))
retentionBaseURL := os.Getenv("NEXARCH_DMS_APP_RETENTION_BASE_URL")
retentionClass := os.Getenv("NEXARCH_DMS_APP_RETENTION_CLASS")
callbackURL := os.Getenv("NEXARCH_DMS_APP_RETENTION_CALLBACK_URL")
if retentionBaseURL == "" || retentionClass == "" || callbackURL == "" {
log.Print("dms-app: RET-05-Registrierung uebersprungen (RETENTION_BASE_URL/CLASS/CALLBACK_URL unvollstaendig)")
return
}
client := retentionclient.New(retentionBaseURL)
queue := jobqueue.NewQueue(pool, 5*time.Minute)
cfg := retentiondestroy.RegisterConfig{
ObjectType: retentiondestroy.ObjectTypeDocument,
RetentionClass: retentionClass,
CallbackURL: callbackURL,
}
regCtx, cancel := context.WithTimeout(ctx, 10*time.Second)
defer cancel()
queued, err := retentiondestroy.RegisterOrRequeue(regCtx, client, queue, cfg)
if err != nil {
log.Printf("dms-app: RET-05-registrierung fehlgeschlagen (requeued=%t): %v", queued, err)
} else {
log.Print("dms-app: bei archive RET-05 registriert")
}
}
+54 -3
View File
@@ -1,16 +1,23 @@
// worker ist der Hintergrund-Dienst des DMS — verarbeitet lange laufende
// Aufgaben (Indexierung, OCR, Storage-Vorgaenge in spaeteren Kacheln),
// getrennt vom App-Prozess (siehe cmd/app).
// getrennt vom App-Prozess (siehe cmd/app). DOC-16: verarbeitet zusaetzlich
// Requeue-Jobs fuer fehlgeschlagene RET-05-Registrierungen.
package main
import (
"context"
"errors"
"log"
"os"
"os/signal"
"syscall"
"time"
"github.com/jackc/pgx/v5/pgxpool"
"gitea.perlbach24.de/scripte/nexarch/dms/internal/jobqueue"
"gitea.perlbach24.de/scripte/nexarch/dms/internal/retentionclient"
"gitea.perlbach24.de/scripte/nexarch/dms/internal/retentiondestroy"
"gitea.perlbach24.de/scripte/nexarch/dms/internal/shared"
)
@@ -20,6 +27,21 @@ func main() {
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
defer stop()
var (
queue *jobqueue.Queue
client *retentionclient.Client
)
if dsn := os.Getenv("NEXARCH_DMS_WORKER_TENANT_DSN"); dsn != "" {
pool, err := pgxpool.New(ctx, dsn)
if err != nil {
log.Fatalf("dms-worker: datenbankverbindung: %v", err)
}
queue = jobqueue.NewQueue(pool, 5*time.Minute)
client = retentionclient.New(os.Getenv("NEXARCH_DMS_WORKER_RETENTION_BASE_URL"))
} else {
log.Print("dms-worker: NEXARCH_DMS_WORKER_TENANT_DSN nicht gesetzt, DOC-16-Requeue-Verarbeitung deaktiviert")
}
ticker := time.NewTicker(30 * time.Second)
defer ticker.Stop()
for {
@@ -28,8 +50,37 @@ func main() {
log.Println("dms-worker beendet")
return
case <-ticker.C:
// Platzhalter fuer Job-Verarbeitung (FDN-02 ff.) — Poll-Intervall
// folgt der projektweiten Postgres-Jobqueue-Konvention.
if queue != nil {
processRetentionRegisterJobs(ctx, queue, client)
}
}
}
}
// processRetentionRegisterJobs holt und verarbeitet ALLE aktuell
// abholbaren doc16_retention_register-Jobs (Akzeptanzkriterium 3: Requeue
// statt Absturz/Verwerfen — Fail() haengt selbst nie ab, sondern setzt
// pending mit Backoff oder dead_letter, siehe FDN-04).
func processRetentionRegisterJobs(ctx context.Context, queue *jobqueue.Queue, client *retentionclient.Client) {
for {
job, err := queue.Dequeue(ctx, "dms-worker", []string{retentiondestroy.JobTypeRegister})
if err != nil {
if !errors.Is(err, jobqueue.ErrNoJobAvailable) {
log.Printf("dms-worker: dequeue fehlgeschlagen: %v", err)
}
return
}
if err := retentiondestroy.ProcessRegisterJob(ctx, client, job.Payload); err != nil {
log.Printf("dms-worker: retention-registrierung erneut fehlgeschlagen (job %s): %v", job.ID, err)
if failErr := queue.Fail(ctx, job.ID, err); failErr != nil {
log.Printf("dms-worker: job als fehlgeschlagen markieren: %v", failErr)
}
continue
}
if err := queue.Complete(ctx, job.ID); err != nil {
log.Printf("dms-worker: job abschliessen: %v", err)
} else {
log.Printf("dms-worker: retention-registrierung erfolgreich nachgeholt (job %s)", job.ID)
}
}
}