Initial import of the CBDDC codebase with docs and tests. Add a .NET-focused gitignore to keep generated artifacts out of source control.
Some checks failed
CI / verify (push) Has been cancelled
Some checks failed
CI / verify (push) Has been cancelled
This commit is contained in:
251
docs/conflict-resolution.md
Executable file
251
docs/conflict-resolution.md
Executable file
@@ -0,0 +1,251 @@
|
||||
# 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)
|
||||
Reference in New Issue
Block a user