package backupengine import ( "sync" "sync/atomic" "time" ) // Progress beschreibt den Stand eines laufenden Backups (PROMPT.md §34). type Progress struct { // BytesProcessed ist die Menge der gelesenen Ursprungsdaten. BytesProcessed int64 `json:"bytes_processed"` // BytesWritten ist die Menge der tatsächlich abgelegten Daten. BytesWritten int64 `json:"bytes_written"` // BytesDeduplicated ist die durch Deduplizierung eingesparte Menge. BytesDeduplicated int64 `json:"bytes_deduplicated"` // ChunksWritten ist die Zahl neu geschriebener Blöcke. ChunksWritten int64 `json:"chunks_written"` // ChunksDeduplicated ist die Zahl bereits vorhandener Blöcke. ChunksDeduplicated int64 `json:"chunks_deduplicated"` // BytesReused ist die Datenmenge, die dank Zusatzsicherung gar nicht // gelesen werden musste. // // Sie steht bewusst neben BytesDeduplicated und nicht darin: ein // deduplizierter Block wurde gelesen und gehasht, ein übernommener nicht // einmal geöffnet. Die beiden Zahlen zu vermengen verschleierte genau den // Gewinn, um dessentwillen es Zusatzsicherungen gibt. BytesReused int64 `json:"bytes_reused"` // ChunksReused ist die Zahl übernommener Blöcke. ChunksReused int64 `json:"chunks_reused"` // ChunksIncompressible ist die Zahl neu geschriebener, nicht verkleinerbarer Blöcke. // // Der Entropie-Indikator (Phase 16). Gezählt werden nur **neue** Blöcke: // Ein deduplizierter stammt aus einem früheren Lauf und sagt nichts über // die Daten von heute. ChunksIncompressible int64 `json:"chunks_incompressible"` // ElapsedSeconds ist die bisher verstrichene Zeit. ElapsedSeconds float64 `json:"elapsed_seconds"` // ThroughputBytesPerSecond ist der aktuelle Durchsatz auf die Ursprungsdaten bezogen. ThroughputBytesPerSecond float64 `json:"throughput_bytes_per_second"` } // CompressionRatio liefert das Verhältnis von Ursprungs- zu abgelegten Daten. // // Der zweite Rückgabewert meldet, ob ein endliches Verhältnis existiert. Wurde // nichts abgelegt, weil jeder Block bereits vorlag, gibt es keines — ein // stillschweigendes 0 stellte den besten Fall als den schlechtesten dar. func (progress Progress) CompressionRatio() (ratio float64, isDefined bool) { if progress.BytesProcessed == 0 || progress.BytesWritten == 0 { return 0, false } return float64(progress.BytesProcessed) / float64(progress.BytesWritten), true } // SavingsPercentage liefert den Anteil eingesparter Daten in Prozent. // // Die Kennzahl ist immer bestimmbar und deshalb für Anzeigen die verlässlichere. func (progress Progress) SavingsPercentage() float64 { if progress.BytesProcessed == 0 { return 0 } savedBytes := progress.BytesProcessed - progress.BytesWritten if savedBytes < 0 { // Bei sehr kleinen oder bereits komprimierten Daten kann die abgelegte // Menge die Ursprungsmenge übersteigen. Eine negative Ersparnis // auszuweisen wäre verwirrend; 0 ist die ehrliche Aussage. return 0 } return float64(savedBytes) / float64(progress.BytesProcessed) * 100 } // ProgressCallback wird während eines Backups mit dem Fortschritt aufgerufen. // // Die Funktion muss schnell zurückkehren: sie läuft im Takt der Verarbeitung // und würde die Pipeline sonst ausbremsen. type ProgressCallback func(progress Progress) // ProgressReporter sammelt die Kennzahlen eines laufenden Backups. // // Die Zähler sind atomar, weil mehrere Arbeiter gleichzeitig melden. type ProgressReporter struct { // bytesProcessed zählt die gelesenen Ursprungsdaten. bytesProcessed atomic.Int64 // bytesWritten zählt die abgelegten Daten. bytesWritten atomic.Int64 // bytesDeduplicated zählt die eingesparten Daten. bytesDeduplicated atomic.Int64 // chunksWritten zählt die neu geschriebenen Blöcke. chunksWritten atomic.Int64 // chunksDeduplicated zählt die bereits vorhandenen Blöcke. chunksDeduplicated atomic.Int64 // bytesReused zählt die ungelesen übernommenen Bytes. bytesReused atomic.Int64 // chunksReused zählt die ungelesen übernommenen Blöcke. chunksReused atomic.Int64 // chunksIncompressible zählt die neu geschriebenen, nicht verkleinerbaren Blöcke. // // Der Entropie-Indikator der Anlage (Phase 16): Normale Nutzdaten lassen // sich fast immer verkleinern, verschlüsselte nie. Ein Sprung in diesem // Anteil ist das billigste verlässliche Signal für massenhafte // Verschlüsselung. chunksIncompressible atomic.Int64 // startTime ist der Beginn des Backups. startTime time.Time // callback meldet den Fortschritt nach außen. callback ProgressCallback // callbackInterval ist der Mindestabstand zweier Meldungen. callbackInterval time.Duration // callbackMutex schützt den Zeitpunkt der letzten Meldung. callbackMutex sync.Mutex // lastCallbackTime ist der Zeitpunkt der letzten Meldung. lastCallbackTime time.Time } // defaultProgressInterval ist der Mindestabstand zweier Fortschrittsmeldungen. // // Ohne Drosselung entstünde bei kleinen Blöcken eine Flut von Aufrufen, die // mehr Zeit kostete als die eigentliche Arbeit. const defaultProgressInterval = 500 * time.Millisecond // NewProgressReporter erzeugt einen Fortschrittszähler. func NewProgressReporter(callback ProgressCallback, callbackInterval time.Duration) *ProgressReporter { if callbackInterval <= 0 { callbackInterval = defaultProgressInterval } return &ProgressReporter{ startTime: time.Now(), callback: callback, callbackInterval: callbackInterval, lastCallbackTime: time.Now(), } } // recordWrittenChunk vermerkt einen neu geschriebenen Block. func (progressReporter *ProgressReporter) recordWrittenChunk(logicalBytes int64, storedBytes int64) { progressReporter.bytesProcessed.Add(logicalBytes) progressReporter.bytesWritten.Add(storedBytes) progressReporter.chunksWritten.Add(1) progressReporter.maybeReport() } // recordIncompressibleChunk vermerkt einen nicht verkleinerbaren Block. func (progressReporter *ProgressReporter) recordIncompressibleChunk() { progressReporter.chunksIncompressible.Add(1) } // recordDeduplicatedChunk vermerkt einen bereits vorhandenen Block. func (progressReporter *ProgressReporter) recordDeduplicatedChunk(logicalBytes int64) { progressReporter.bytesProcessed.Add(logicalBytes) progressReporter.bytesDeduplicated.Add(logicalBytes) progressReporter.chunksDeduplicated.Add(1) progressReporter.maybeReport() } // recordReusedChunk vermerkt einen aus dem Elternbackup übernommenen Block. // // Er zählt nicht zu den verarbeiteten Bytes: gelesen wurde er nie. Stünde er // dort, gäbe der Durchsatz eine Leseleistung aus, die es nicht gab. func (progressReporter *ProgressReporter) recordReusedChunk(logicalBytes int64) { progressReporter.bytesReused.Add(logicalBytes) progressReporter.chunksReused.Add(1) progressReporter.maybeReport() } // maybeReport meldet den Fortschritt, sofern der Mindestabstand verstrichen ist. func (progressReporter *ProgressReporter) maybeReport() { if progressReporter.callback == nil { return } progressReporter.callbackMutex.Lock() if time.Since(progressReporter.lastCallbackTime) < progressReporter.callbackInterval { progressReporter.callbackMutex.Unlock() return } progressReporter.lastCallbackTime = time.Now() progressReporter.callbackMutex.Unlock() // Die Meldung erfolgt außerhalb der Sperre: eine langsame Rückrufmethode // soll die übrigen Arbeiter nicht blockieren. progressReporter.callback(progressReporter.Snapshot()) } // Snapshot liefert den aktuellen Stand. func (progressReporter *ProgressReporter) Snapshot() Progress { elapsedDuration := time.Since(progressReporter.startTime) elapsedSeconds := elapsedDuration.Seconds() bytesProcessed := progressReporter.bytesProcessed.Load() var throughput float64 if elapsedSeconds > 0 { throughput = float64(bytesProcessed) / elapsedSeconds } return Progress{ BytesProcessed: bytesProcessed, BytesWritten: progressReporter.bytesWritten.Load(), BytesDeduplicated: progressReporter.bytesDeduplicated.Load(), ChunksWritten: progressReporter.chunksWritten.Load(), ChunksDeduplicated: progressReporter.chunksDeduplicated.Load(), BytesReused: progressReporter.bytesReused.Load(), ChunksReused: progressReporter.chunksReused.Load(), ChunksIncompressible: progressReporter.chunksIncompressible.Load(), ElapsedSeconds: elapsedSeconds, ThroughputBytesPerSecond: throughput, } } // ReportFinal meldet den Endstand unabhängig vom Mindestabstand. func (progressReporter *ProgressReporter) ReportFinal() Progress { finalProgress := progressReporter.Snapshot() if progressReporter.callback != nil { progressReporter.callback(finalProgress) } return finalProgress }