Files
CBDDC/docs/conflict-resolution.md

252 lines
6.8 KiB
Markdown
Executable File

# Conflict Resolution
**CBDDC v0.6.0** introduces **pluggable conflict resolution strategies** to handle concurrent updates to the same document across different nodes.
## Overview
When two nodes modify the same document offline and later sync, a conflict occurs. CBDDC provides two built-in strategies:
1. **Last Write Wins (LWW)** - Simple, timestamp-based resolution
2. **Recursive Merge** - Intelligent JSON merging with array handling
## Conflict Resolution Strategies
### Last Write Wins (LWW)
**How it works:**
- Each document has a Hybrid Logical Clock (HLC) timestamp
- During sync, the document with the **highest timestamp wins**
- Conflicts are resolved automatically with no merge attempt
**Pros:**
- ✅ Simple and predictable
- ✅ Fast (no merge computation)
- ✅ No data corruption or invalid states
**Cons:**
- ❌ Data loss - one change is discarded entirely
- ❌ Not suitable for collaborative editing
**Use Cases:**
- Configuration data with infrequent updates
- Reference data (product catalogs, price lists)
- Single-user scenarios with backup sync
#### Example
```csharp
// Both nodes start with same document
{ "id": "doc-1", "name": "Alice", "age": 25 }
// Node A updates (timestamp: 100)
{ "id": "doc-1", "name": "Alice", "age": 26 }
// Node B updates (timestamp: 105)
{ "id": "doc-1", "name": "Alicia", "age": 25 }
// After sync: Node B wins (higher timestamp)
{ "id": "doc-1", "name": "Alicia", "age": 25 }
// Node A's age change is LOST
```
---
### Recursive Merge
**How it works:**
- Performs deep JSON merge of conflicting documents
- Uses the **highest timestamp** for each individual field
- Arrays with `id` or `_id` fields are merged by identity
- Arrays without IDs are concatenated and deduplicated
**Pros:**
- ✅ Preserves changes from both nodes
- ✅ Suitable for collaborative scenarios
- ✅ Intelligent array handling
**Cons:**
- ❌ More complex logic
- ❌ Slightly slower (~5-10ms overhead)
- ❌ May produce unexpected results with complex nested structures
**Use Cases:**
- TodoLists, shopping carts (demonstrated in samples)
- Collaborative documents
- Complex objects with independent fields
- Data where every change matters
#### Example: Field-Level Merge
```csharp
// Both nodes start with same document
{ "id": "doc-1", "name": "Alice", "age": 25 }
// Node A updates (timestamp: 100)
{ "id": "doc-1", "name": "Alice", "age": 26 }
// Node B updates (timestamp: 105)
{ "id": "doc-1", "name": "Alicia", "age": 25 }
// After Recursive Merge:
{ "id": "doc-1", "name": "Alicia", "age": 26 }
// Uses latest timestamp for each field independently
```
#### Example: Array Merging with IDs
```csharp
// Initial TodoList
{
"id": "list-1",
"name": "Shopping",
"items": [
{ "id": "1", "task": "Buy milk", "completed": false },
{ "id": "2", "task": "Buy bread", "completed": false }
]
}
// Node A: Completes milk, adds eggs
{
"items": [
{ "id": "1", "task": "Buy milk", "completed": true },
{ "id": "2", "task": "Buy bread", "completed": false },
{ "id": "3", "task": "Buy eggs", "completed": false }
]
}
// Node B: Completes bread, adds cheese
{
"items": [
{ "id": "1", "task": "Buy milk", "completed": false },
{ "id": "2", "task": "Buy bread", "completed": true },
{ "id": "4", "task": "Buy cheese", "completed": false }
]
}
// After Recursive Merge: ALL changes preserved!
{
"items": [
{ "id": "1", "task": "Buy milk", "completed": true }, // A's completion
{ "id": "2", "task": "Buy bread", "completed": true }, // B's completion
{ "id": "3", "task": "Buy eggs", "completed": false }, // A's addition
{ "id": "4", "task": "Buy cheese", "completed": false } // B's addition
]
}
```
## Configuration
### Select Strategy at Startup
```csharp
using ZB.MOM.WW.CBDDC.Core.Sync;
// Last Write Wins (default)
services.AddSingleton<IConflictResolver, LastWriteWinsConflictResolver>();
// OR Recursive Merge
services.AddSingleton<IConflictResolver, RecursiveNodeMergeConflictResolver>();
```
### Console Sample
```bash
# Use Recursive Merge
dotnet run --merge
# Use Last Write Wins (default)
dotnet run
```
### UI Clients
Resolver selection can be exposed in your UI:
1. Choose "Recursive Merge" or "Last Write Wins"
2. Save configuration
3. Restart the application if required by your host
## Interactive Demo
A UI demo can expose a **"Run Conflict Demo"** action that:
1. Creates a TodoList with 2 items
2. Simulates concurrent edits from two "nodes"
3. Shows the merged result
4. Compares LWW vs Recursive Merge behavior
**Try it yourself** to see the difference!
## Best Practices
### Use LWW When:
- Only one user/node typically writes
- Data is reference/configuration
- Simplicity is more important than preserving every change
- Performance is critical
### Use Recursive Merge When:
- Multiple users collaborate
- Every change is valuable (e.g., TodoItems, cart items)
- Data has independent fields that can conflict
- Arrays have `id` fields for identity
### Avoid Conflicts Entirely:
- Use **different collections** for different data types
- Implement **optimistic locking** with version fields
- Design data models to minimize overlapping writes
## Custom Resolvers
You can implement `IConflictResolver` for custom logic:
```csharp
public class CustomResolver : IConflictResolver
{
public ValueTask<string> ResolveConflict(
string localJson,
string remoteJson,
long localTimestamp,
long remoteTimestamp)
{
// Your custom merge logic here
return new ValueTask<string>(result);
}
}
```
Register your resolver:
```csharp
services.AddSingleton<IConflictResolver, CustomResolver>();
```
## Performance
Benchmark results (1000 conflict resolutions):
| Strategy | Avg Time | Throughput |
|----------|----------|------------|
| Last Write Wins | 0.05ms | 20,000 ops/sec |
| Recursive Merge | 0.15ms | 6,600 ops/sec |
**Recommendation**: Performance difference is negligible for most applications. Choose based on data preservation needs.
## FAQ
**Q: Can I switch resolvers at runtime?**
A: No. The resolver is injected at startup. Changing requires application restart.
**Q: What happens if I change resolvers after data exists?**
A: Existing data is unaffected. Only future conflicts use the new strategy.
**Q: Can different nodes use different resolvers?**
A: Technically yes, but **not recommended**. All nodes should use the same strategy for consistency.
**Q: Does this handle schema changes?**
A: No. Conflict resolution assumes both documents have compatible schemas.
---
**See Also:**
- [Getting Started](getting-started.html)
- [Architecture](architecture.html)
- [Security](security.html)