PatchLog.kt
package de.werkbaum.service
import de.werkbaum.domain.ContentPatchOutcome
import java.util.UUID
/**
* Merkt sich je (Dokument, Client) die zuletzt verarbeitete Sequenznummer samt
* Ergebnis – damit ein wiederholtes Einreichen nicht ein zweites Mal wirkt.
*
* Geht die Antwort unterwegs verloren – im Mobilnetz der Normalfall –, weiß
* der Client nicht, ob seine Änderung ankam (D76). Kurzlebig und im Speicher:
* Das Fenster ist Sekunden lang, und eine Einzelinstanz ist ohnehin
* vorausgesetzt. Die Zahl der gemerkten Paare ist gedeckelt; verdrängt wird
* das am längsten nicht benutzte.
*
* Alle Methoden sind synchronisiert: Aufrufe kommen aus verschiedenen
* Dokument-Sperren und damit echt nebenläufig.
*/
class PatchLog(private val capacity: Int = 1_024) {
private data class Seen(val seq: Long, val outcome: ContentPatchOutcome)
private val entries = object : LinkedHashMap<Pair<UUID, String>, Seen>(64, 0.75f, true) {
override fun removeEldestEntry(eldest: Map.Entry<Pair<UUID, String>, Seen>) =
size > capacity
}
/**
* `null` heißt: neu, bitte anwenden. Ein Ergebnis heißt: schon erledigt,
* das war die Antwort. Eine **kleinere** Sequenznummer als die zuletzt
* verarbeitete ist ein Client-Fehler – das Ergebnis von damals ist nicht
* mehr bekannt, und ein zweites Anwenden verdürbe den Text.
*/
@Synchronized
fun outcomeOf(documentId: UUID, clientId: String, seq: Long): ContentPatchOutcome? {
val seen = entries[documentId to clientId] ?: return null
return when {
seq > seen.seq -> null
seq == seen.seq -> seen.outcome
else -> throw StalePatchSequenceException(seq, seen.seq)
}
}
@Synchronized
fun record(documentId: UUID, clientId: String, seq: Long, outcome: ContentPatchOutcome) {
entries[documentId to clientId] = Seen(seq, outcome)
}
@Synchronized
fun size(): Int = entries.size
}
/** Eine ältere Sequenznummer als die zuletzt verarbeitete (422). */
class StalePatchSequenceException(seq: Long, lastSeen: Long) :
RuntimeException("Sequenznummer $seq ist veraltet (zuletzt verarbeitet: $lastSeen)")