Conflict Resolution (Çakışma Çözümü)
Mobil uygulamalarda çoklu cihaz kullanımı, offline düzenleme ve eşzamanlı veri değişiklikleri nedeniyle ortaya çıkan veri çakışmalarının çözümlenmesi kritik önem taşır. Bu bölümde, basit timestamp-based çözümlerden karmaşık CRDT implementasyonlarına kadar çeşitli conflict resolution stratejileri ele alınmaktadır.
Conflict Scenarios
Tipik Çakışma Senaryoları
Multi-Device Editing
// Aynı dökümanın farklı cihazlarda düzenlenmesi
interface DocumentConflict {
documentId: string;
localVersion: DocumentVersion;
remoteVersion: DocumentVersion;
conflictFields: string[];
}
class MultiDeviceConflictDetector {
detectConflicts(local: Document, remote: Document): DocumentConflict | null {
if (local.lastModified === remote.lastModified) {
return null; // No conflict
}
const conflictFields = this.findConflictingFields(local, remote);
if (conflictFields.length === 0) {
return null; // Different timestamps but no actual conflicts
}
return {
documentId: local.id,
localVersion: local.version,
remoteVersion: remote.version,
conflictFields
};
}
private findConflictingFields(local: Document, remote: Document): string[] {
const conflicts: string[] = [];
for (const field in local.data) {
if (local.data[field] !== remote.data[field]) {
conflicts.push(field);
}
}
return conflicts;
}
}
Collaborative Editing Real-time işbirlikçi düzenleme senaryoları.
// Android Collaborative Editing Conflict
data class EditOperation(
val id: String,
val userId: String,
val timestamp: Long,
val type: OperationType,
val position: Int,
val content: String,
val vectorClock: VectorClock
)
class CollaborativeConflictResolver {
fun resolveOperationConflicts(
localOps: List<EditOperation>,
remoteOps: List<EditOperation>
): List<EditOperation> {
val allOps = (localOps + remoteOps).sortedBy { it.timestamp }
val resolvedOps = mutableListOf<EditOperation>()
for (op in allOps) {
val transformedOp = transformOperation(op, resolvedOps)
resolvedOps.add(transformedOp)
}
return resolvedOps
}
private fun transformOperation(
op: EditOperation,
appliedOps: List<EditOperation>
): EditOperation {
var transformedOp = op
for (appliedOp in appliedOps) {
if (needsTransformation(transformedOp, appliedOp)) {
transformedOp = operationalTransform(transformedOp, appliedOp)
}
}
return transformedOp
}
}
Simple Resolution Strategies
Last-Write-Wins (LWW)
En basit conflict resolution stratejisi.
LWW Problems
- Data loss riski
- Concurrent edits'te poor user experience
- Causality tracking eksikliği
First-Write-Wins
Lock-based yaklaşım ile first writer advantage.
// Flutter First-Write-Wins with Optimistic Locking
class OptimisticLockResolver {
Future<WriteResult> attemptWrite(
String documentId,
Map<String, dynamic> changes,
int expectedVersion
) async {
try {
final currentDoc = await repository.getDocument(documentId);
if (currentDoc.version != expectedVersion) {
return WriteResult.conflict(
message: 'Document was modified by another user',
currentVersion: currentDoc.version,
expectedVersion: expectedVersion
);
}
final updatedDoc = currentDoc.copyWith(
data: {...currentDoc.data, ...changes},
version: currentDoc.version + 1,
lastModified: DateTime.now()
);
await repository.updateDocument(updatedDoc);
return WriteResult.success(updatedDoc);
} catch (e) {
return WriteResult.error(e.toString());
}
}
}
sealed class WriteResult {
const WriteResult();
factory WriteResult.success(Document doc) = WriteSuccess;
factory WriteResult.conflict({
required String message,
required int currentVersion,
required int expectedVersion
}) = WriteConflict;
factory WriteResult.error(String message) = WriteError;
}
Custom Business Logic Resolution
Domain-specific conflict resolution kuralları.
class BusinessLogicResolver {
fun resolveUserProfileConflict(
local: UserProfile,
remote: UserProfile
): UserProfile {
return UserProfile(
id = local.id,
// Email: remote always wins (server-side validation)
email = remote.email,
// Name: most recent wins
name = if (local.nameUpdatedAt > remote.nameUpdatedAt)
local.name else remote.name,
// Settings: merge strategies
settings = mergeSettings(local.settings, remote.settings),
// Avatar: keep higher resolution
avatar = selectBestAvatar(local.avatar, remote.avatar),
// Subscription: server always wins
subscription = remote.subscription
)
}
private fun mergeSettings(
local: UserSettings,
remote: UserSettings
): UserSettings {
return UserSettings(
// Notifications: union of preferences
notifications = local.notifications + remote.notifications,
// Theme: most recent
theme = if (local.themeUpdatedAt > remote.themeUpdatedAt)
local.theme else remote.theme,
// Privacy: most restrictive
privacy = selectMostRestrictive(local.privacy, remote.privacy)
)
}
}
CRDT (Conflict-free Replicated Data Types)
State-based CRDTs
State merge ile conflict-free replication.
// G-Counter (Grow-only Counter) CRDT
class GCounter {
constructor(actorId) {
this.actorId = actorId;
this.counters = new Map();
this.counters.set(actorId, 0);
}
increment() {
const current = this.counters.get(this.actorId) || 0;
this.counters.set(this.actorId, current + 1);
}
merge(other) {
const merged = new GCounter(this.actorId);
// Get all actor IDs from both counters
const allActors = new Set([
...this.counters.keys(),
...other.counters.keys()
]);
// Take maximum value for each actor
for (const actor of allActors) {
const thisValue = this.counters.get(actor) || 0;
const otherValue = other.counters.get(actor) || 0;
merged.counters.set(actor, Math.max(thisValue, otherValue));
}
return merged;
}
value() {
return Array.from(this.counters.values()).reduce((sum, val) => sum + val, 0);
}
}
// PN-Counter (Increment/Decrement Counter)
class PNCounter {
constructor(actorId) {
this.positive = new GCounter(actorId);
this.negative = new GCounter(actorId);
}
increment() {
this.positive.increment();
}
decrement() {
this.negative.increment();
}
merge(other) {
const merged = new PNCounter(this.positive.actorId);
merged.positive = this.positive.merge(other.positive);
merged.negative = this.negative.merge(other.negative);
return merged;
}
value() {
return this.positive.value() - this.negative.value();
}
}
Operation-based CRDTs
Operation replication ile conflict resolution.
// iOS LWW-Element-Set CRDT
struct LWWElementSet<Element: Hashable> {
private var added: [Element: TimeVector] = [:]
private var removed: [Element: TimeVector] = [:]
private let actorId: String
init(actorId: String) {
self.actorId = actorId
}
mutating func add(_ element: Element) {
let timestamp = TimeVector.now(actorId: actorId)
added[element] = timestamp
}
mutating func remove(_ element: Element) {
let timestamp = TimeVector.now(actorId: actorId)
removed[element] = timestamp
}
func contains(_ element: Element) -> Bool {
guard let addedTime = added[element] else { return false }
if let removedTime = removed[element] {
return addedTime > removedTime
}
return true
}
func merge(with other: LWWElementSet<Element>) -> LWWElementSet<Element> {
var merged = LWWElementSet<Element>(actorId: actorId)
// Merge added timestamps
for (element, timestamp) in added {
if let otherTimestamp = other.added[element] {
merged.added[element] = max(timestamp, otherTimestamp)
} else {
merged.added[element] = timestamp
}
}
for (element, timestamp) in other.added {
if merged.added[element] == nil {
merged.added[element] = timestamp
}
}
// Merge removed timestamps
for (element, timestamp) in removed {
if let otherTimestamp = other.removed[element] {
merged.removed[element] = max(timestamp, otherTimestamp)
} else {
merged.removed[element] = timestamp
}
}
for (element, timestamp) in other.removed {
if merged.removed[element] == nil {
merged.removed[element] = timestamp
}
}
return merged
}
}
Text Editing CRDTs
Collaborative text editing için özel CRDT'ler.
// RGA (Replicated Growable Array) for Text
data class RGAChar(
val char: Char,
val id: CharacterId,
val isVisible: Boolean = true
)
data class CharacterId(
val actorId: String,
val sequence: Long,
val offset: Int = 0
) : Comparable<CharacterId> {
override fun compareTo(other: CharacterId): Int {
return when {
sequence != other.sequence -> sequence.compareTo(other.sequence)
actorId != other.actorId -> actorId.compareTo(other.actorId)
else -> offset.compareTo(other.offset)
}
}
}
class RGAString(private val actorId: String) {
private val chars = mutableListOf<RGAChar>()
private var sequenceCounter = 0L
fun insert(position: Int, char: Char) {
val id = CharacterId(actorId, ++sequenceCounter)
val rgaChar = RGAChar(char, id)
// Find insertion point in RGA structure
val insertionIndex = findInsertionIndex(position, id)
chars.add(insertionIndex, rgaChar)
}
fun delete(position: Int) {
val visiblePosition = getVisiblePosition(position)
if (visiblePosition < chars.size) {
chars[visiblePosition] = chars[visiblePosition].copy(isVisible = false)
}
}
fun merge(operations: List<RGAOperation>) {
for (op in operations.sortedBy { it.id }) {
when (op.type) {
RGAOperationType.INSERT -> applyInsert(op)
RGAOperationType.DELETE -> applyDelete(op)
}
}
}
fun toString(): String {
return chars.filter { it.isVisible }.joinToString("") { it.char.toString() }
}
private fun findInsertionIndex(position: Int, id: CharacterId): Int {
// Complex logic to maintain RGA invariants
// This ensures convergence across all replicas
var index = 0
var visibleCount = 0
while (index < chars.size && visibleCount < position) {
if (chars[index].isVisible) {
visibleCount++
}
if (visibleCount < position) {
index++
}
}
// Handle concurrent inserts at same position
while (index < chars.size && chars[index].id > id) {
index++
}
return index
}
}
Platform-Specific Implementations
iOS Core Data Merge Policies
Core Data'da built-in conflict resolution.
class CoreDataConflictResolver {
func setupMergePolicies() {
let container = NSPersistentContainer(name: "DataModel")
// Configure merge policy
container.persistentStoreDescriptions.first?.setOption(
true as NSNumber,
forKey: NSPersistentHistoryTrackingKey
)
container.persistentStoreDescriptions.first?.setOption(
true as NSNumber,
forKey: NSPersistentStoreRemoteChangeNotificationPostOptionKey
)
container.viewContext.mergePolicy = NSMergePolicy.mergeByPropertyObjectTrump
}
func customMergePolicy() -> NSMergePolicy {
return NSMergePolicy(merge: .mergeByPropertyObjectTrumpMergePolicyType) { conflict in
// Custom resolution logic
for case let object as User in conflict.sourceObject {
if let snapshot = conflict.cachedSnapshot {
// Field-level conflict resolution
self.resolveUserConflict(object: object, snapshot: snapshot)
}
}
return true
}
}
private func resolveUserConflict(object: User, snapshot: [String: Any]) {
// Email: server wins
if let serverEmail = snapshot["email"] as? String {
object.email = serverEmail
}
// Name: most recent timestamp wins
if let localTimestamp = object.nameUpdatedAt,
let serverTimestamp = snapshot["nameUpdatedAt"] as? Date {
if serverTimestamp > localTimestamp {
object.name = snapshot["name"] as? String
object.nameUpdatedAt = serverTimestamp
}
}
// Settings: merge
if let serverSettings = snapshot["settings"] as? Data {
object.settings = mergeUserSettings(
local: object.settings,
remote: serverSettings
)
}
}
}
Android Room Conflict Resolution
Room database ile conflict handling.
@Dao
interface UserDao {
@Insert(onConflict = OnConflictStrategy.IGNORE)
suspend fun insertIgnoreConflict(user: User): Long
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertReplaceConflict(user: User): Long
@Query("SELECT * FROM users WHERE id = :id")
suspend fun getUserById(id: String): User?
@Transaction
suspend fun resolveUserConflict(localUser: User, remoteUser: User): User {
val resolved = ConflictResolver.resolveUser(localUser, remoteUser)
update(resolved)
return resolved
}
}
class RoomConflictResolver {
suspend fun handleSyncConflicts(conflicts: List<SyncConflict>) {
for (conflict in conflicts) {
when (conflict.entity) {
"User" -> resolveUserConflict(conflict)
"Document" -> resolveDocumentConflict(conflict)
"Settings" -> resolveSettingsConflict(conflict)
}
}
}
private suspend fun resolveUserConflict(conflict: SyncConflict) {
val local = userDao.getUserById(conflict.entityId) ?: return
val remote = conflict.remoteData.toUser()
val resolved = when (conflict.resolutionStrategy) {
ResolutionStrategy.LAST_WRITE_WINS -> {
if (local.lastModified > remote.lastModified) local else remote
}
ResolutionStrategy.FIELD_LEVEL_MERGE -> {
User(
id = local.id,
email = remote.email, // Server always wins
name = if (local.nameUpdatedAt > remote.nameUpdatedAt)
local.name else remote.name,
settings = mergeSettings(local.settings, remote.settings)
)
}
ResolutionStrategy.USER_CHOICE -> {
// Present UI for user to choose
presentConflictResolutionUI(local, remote)
return // Will be resolved asynchronously
}
}
userDao.update(resolved)
}
}
Flutter Conflict Resolution
Cross-platform conflict handling.
class FlutterConflictResolver {
final ConflictResolutionStrategy strategy;
FlutterConflictResolver(this.strategy);
Future<T> resolveConflict<T extends Syncable>(
T local,
T remote,
{ConflictContext? context}
) async {
switch (strategy) {
case ConflictResolutionStrategy.lastWriteWins:
return _resolveLastWriteWins(local, remote);
case ConflictResolutionStrategy.firstWriteWins:
return local; // Local is always first in this context
case ConflictResolutionStrategy.mergeFields:
return _mergeFields(local, remote);
case ConflictResolutionStrategy.userChoice:
return await _presentUserChoice(local, remote, context);
case ConflictResolutionStrategy.crdt:
return _applyCRDTResolution(local, remote);
}
}
T _resolveLastWriteWins<T extends Syncable>(T local, T remote) {
return local.lastModified.isAfter(remote.lastModified) ? local : remote;
}
T _mergeFields<T extends Syncable>(T local, T remote) {
final Map<String, dynamic> localJson = local.toJson();
final Map<String, dynamic> remoteJson = remote.toJson();
final Map<String, dynamic> merged = {};
// Get all unique field names
final allFields = {...localJson.keys, ...remoteJson.keys};
for (final field in allFields) {
final localValue = localJson[field];
final remoteValue = remoteJson[field];
// Field-specific merge logic
merged[field] = _mergeField(field, localValue, remoteValue, local, remote);
}
return local.fromJson(merged) as T;
}
dynamic _mergeField(
String fieldName,
dynamic localValue,
dynamic remoteValue,
Syncable local,
Syncable remote
) {
// Define field-specific merge strategies
switch (fieldName) {
case 'email':
case 'phone':
// Server-validated fields: remote wins
return remoteValue;
case 'preferences':
// Merge maps/objects
if (localValue is Map && remoteValue is Map) {
return {...localValue, ...remoteValue};
}
return remoteValue;
case 'tags':
case 'categories':
// Merge lists (union)
if (localValue is List && remoteValue is List) {
return [...localValue, ...remoteValue].toSet().toList();
}
return remoteValue;
default:
// Default: most recent timestamp wins
final localTimestamp = local.getFieldTimestamp(fieldName);
final remoteTimestamp = remote.getFieldTimestamp(fieldName);
if (localTimestamp != null && remoteTimestamp != null) {
return localTimestamp.isAfter(remoteTimestamp) ? localValue : remoteValue;
}
return remoteValue; // Fallback to remote
}
}
Future<T> _presentUserChoice<T extends Syncable>(
T local,
T remote,
ConflictContext? context
) async {
if (context?.isBackground == true) {
// Can't show UI in background, use fallback strategy
return _resolveLastWriteWins(local, remote);
}
// Show conflict resolution dialog
final result = await showDialog<ConflictResolution>(
context: context?.buildContext ?? Get.context!,
builder: (context) => ConflictResolutionDialog(
local: local,
remote: remote,
),
);
switch (result?.choice) {
case ConflictChoice.keepLocal:
return local;
case ConflictChoice.keepRemote:
return remote;
case ConflictChoice.merge:
return _mergeFields(local, remote);
case null:
// User cancelled, default to LWW
return _resolveLastWriteWins(local, remote);
}
}
}
Advanced Resolution Techniques
Machine Learning-based Resolution
AI ile conflict resolution pattern learning.
class MLConflictResolver:
def __init__(self):
self.model = self.load_trained_model()
def resolve_with_ml(self, conflict_data):
# Feature extraction from conflict
features = self.extract_features(conflict_data)
# Predict best resolution strategy
strategy_prediction = self.model.predict([features])
# Apply predicted strategy
return self.apply_strategy(conflict_data, strategy_prediction)
def extract_features(self, conflict):
return [
conflict.user_engagement_score,
conflict.data_freshness_score,
conflict.conflict_complexity,
conflict.user_preference_history,
conflict.context_importance
]
Semantic Conflict Detection
İçerik analizi ile semantic conflict'ler.
class SemanticConflictDetector {
private let nlProcessor: NaturalLanguageProcessor
func detectSemanticConflicts(
localText: String,
remoteText: String
) -> SemanticConflictResult {
let localTokens = nlProcessor.tokenize(localText)
let remoteTokens = nlProcessor.tokenize(remoteText)
let semanticSimilarity = calculateSimilarity(localTokens, remoteTokens)
if semanticSimilarity < 0.3 {
return .majorConflict(
suggestion: generateMergesuggestion(localText, remoteText)
)
} else if semanticSimilarity < 0.7 {
return .minorConflict(
autoMerge: attemptAutoMerge(localText, remoteText)
)
} else {
return .noConflict
}
}
}
Conflict resolution, modern mobil uygulamalarda kullanıcı deneyimi ve veri bütünlüğü açısından kritik önem taşır. Doğru strateji seçimi, uygulama tipine, kullanıcı davranışlarına ve business requirements'lara bağlı olarak belirlenmelidir. CRDT'ler gibi matematiksel olarak conflict-free yaklaşımlar, complex collaboration senaryolarında tercih edilirken, basit LWW stratejileri çoğu durumda yeterli olabilir.