Skip to content

Commit 9ee26c2

Browse files
committed
Merge branch 'suspicious-ishizaka' into develop
2 parents 9f2906d + 7690464 commit 9ee26c2

File tree

5 files changed

+666
-4
lines changed

5 files changed

+666
-4
lines changed

convex-core/src/main/java/convex/lattice/fs/DLFSLattice.java

Lines changed: 52 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ public AVector<ACell> merge(AVector<ACell> ownValue, AVector<ACell> otherValue)
5656
// Get timestamps from both nodes
5757
CVMLong timeA = DLFSNode.getUTime(ownValue);
5858
CVMLong timeB = DLFSNode.getUTime(otherValue);
59-
59+
6060
// Use the maximum timestamp for the merge operation
6161
// This ensures merged nodes have a timestamp that reflects the most recent change
6262
CVMLong mergeTime = timeA.longValue() >= timeB.longValue() ? timeA : timeB;
@@ -65,6 +65,37 @@ public AVector<ACell> merge(AVector<ACell> ownValue, AVector<ACell> otherValue)
6565
return DLFSNode.merge(ownValue, otherValue, mergeTime);
6666
}
6767

68+
@Override
69+
public AVector<ACell> merge(convex.lattice.LatticeContext context, AVector<ACell> ownValue, AVector<ACell> otherValue) {
70+
// Handle null cases
71+
if (ownValue == null) {
72+
if (checkForeign(otherValue)) {
73+
return otherValue;
74+
}
75+
return zero();
76+
}
77+
if (otherValue == null) {
78+
return ownValue;
79+
}
80+
81+
// Fast path: if values are equal, return own value
82+
if (Utils.equals(ownValue, otherValue)) {
83+
return ownValue;
84+
}
85+
86+
// Get merge timestamp from context, fallback to node timestamps
87+
CVMLong mergeTime = context.getTimestamp();
88+
if (mergeTime == null) {
89+
// Fallback: use max of node timestamps
90+
CVMLong timeA = DLFSNode.getUTime(ownValue);
91+
CVMLong timeB = DLFSNode.getUTime(otherValue);
92+
mergeTime = timeA.longValue() >= timeB.longValue() ? timeA : timeB;
93+
}
94+
95+
// Delegate to DLFSNode.merge which implements the rsync-like merge logic
96+
return DLFSNode.merge(ownValue, otherValue, mergeTime);
97+
}
98+
6899
@Override
69100
public AVector<ACell> zero() {
70101
// Zero value is an empty directory
@@ -145,12 +176,30 @@ public Index<AString, AVector<ACell>> merge(Index<AString, AVector<ACell>> ownVa
145176
if (otherValue == null) {
146177
return ownValue;
147178
}
148-
179+
149180
// Merge directory entries using DLFSLattice for values
150181
MergeFunction<AVector<ACell>> mergeFunction = (a, b) -> {
151182
return DLFSLattice.INSTANCE.merge(a, b);
152183
};
153-
184+
185+
return ownValue.mergeDifferences(otherValue, mergeFunction);
186+
}
187+
188+
@Override
189+
public Index<AString, AVector<ACell>> merge(convex.lattice.LatticeContext context, Index<AString, AVector<ACell>> ownValue, Index<AString, AVector<ACell>> otherValue) {
190+
if (ownValue == null) {
191+
if (otherValue == null) return zero();
192+
return otherValue;
193+
}
194+
if (otherValue == null) {
195+
return ownValue;
196+
}
197+
198+
// Merge directory entries using context-aware DLFSLattice for values
199+
MergeFunction<AVector<ACell>> mergeFunction = (a, b) -> {
200+
return DLFSLattice.INSTANCE.merge(context, a, b);
201+
};
202+
154203
return ownValue.mergeDifferences(otherValue, mergeFunction);
155204
}
156205

convex-core/src/main/java/convex/lattice/generic/OwnerLattice.java

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,11 +68,34 @@ public AHashMap<ACell, SignedData<V>> merge(
6868
}
6969
return zero();
7070
}
71-
71+
7272
// Merge the maps using the SignedLattice merge function for each owner
7373
return ownValue.mergeDifferences(otherValue, mergeFunction);
7474
}
7575

76+
@Override
77+
public AHashMap<ACell, SignedData<V>> merge(
78+
convex.lattice.LatticeContext context,
79+
AHashMap<ACell, SignedData<V>> ownValue,
80+
AHashMap<ACell, SignedData<V>> otherValue) {
81+
if (otherValue == null) {
82+
return ownValue;
83+
}
84+
if (ownValue == null) {
85+
if (checkForeign(otherValue)) {
86+
return otherValue;
87+
}
88+
return zero();
89+
}
90+
91+
// Merge the maps using context-aware SignedLattice merge for each owner
92+
MergeFunction<SignedData<V>> contextMergeFunction = (a, b) -> {
93+
return signedLattice.merge(context, a, b);
94+
};
95+
96+
return ownValue.mergeDifferences(otherValue, contextMergeFunction);
97+
}
98+
7699
@Override
77100
public AHashMap<ACell, SignedData<V>> zero() {
78101
return Maps.empty();

convex-core/src/test/java/convex/lattice/fs/DLFSTest.java

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
import convex.core.data.Strings;
3939
import convex.core.data.prim.CVMLong;
4040
import convex.lattice.ALattice;
41+
import convex.lattice.LatticeContext;
4142
import convex.lattice.LatticeTest;
4243

4344
public class DLFSTest {
@@ -547,4 +548,95 @@ public void testDLFSLatticeGenericProperties() throws IOException {
547548
LatticeTest.doLatticeTest(lattice, node1, node2, 0L, dirName);
548549
}
549550

551+
/**
552+
* Test that DLFSLattice context-aware merge uses timestamp from context.
553+
*
554+
* Verifies:
555+
* - Context timestamp is used for merge operations when provided
556+
* - Fallback to node timestamps when context timestamp is null
557+
* - Context timestamp overrides node timestamps for conflict resolution
558+
*/
559+
@Test
560+
public void testDLFSLatticeContextAwareMerge() throws IOException {
561+
DLFSLattice lattice = DLFSLattice.INSTANCE;
562+
563+
// Create two filesystems with conflicting files at different timestamps
564+
DLFileSystem fs1 = DLFS.createLocal();
565+
fs1.setTimestamp(CVMLong.create(1000));
566+
Path file1 = Files.createFile(fs1.getPath("conflict.txt"));
567+
try (OutputStream os = Files.newOutputStream(file1)) {
568+
os.write(new byte[] {1, 2, 3});
569+
}
570+
AVector<ACell> node1 = fs1.getNode(fs1.getRoot());
571+
572+
DLFileSystem fs2 = DLFS.createLocal();
573+
fs2.setTimestamp(CVMLong.create(2000));
574+
Path file2 = Files.createFile(fs2.getPath("conflict.txt"));
575+
try (OutputStream os = Files.newOutputStream(file2)) {
576+
os.write(new byte[] {4, 5, 6});
577+
}
578+
AVector<ACell> node2 = fs2.getNode(fs2.getRoot());
579+
580+
// Test 1: Context with timestamp = 3000
581+
// Should use context timestamp for merge
582+
CVMLong contextTime = CVMLong.create(3000);
583+
LatticeContext context = LatticeContext.create(contextTime, null);
584+
585+
AVector<ACell> merged = lattice.merge(context, node1, node2);
586+
assertNotNull(merged, "Merged value should not be null");
587+
588+
// Merged root should have timestamp from context (3000), not from nodes
589+
CVMLong mergedTime = DLFSNode.getUTime(merged);
590+
assertEquals(3000L, mergedTime.longValue(), "Merged root should use context timestamp");
591+
592+
// Test 2: Context with null timestamp
593+
// Should fall back to max of node timestamps (2000)
594+
LatticeContext emptyContext = LatticeContext.EMPTY;
595+
596+
AVector<ACell> merged2 = lattice.merge(emptyContext, node1, node2);
597+
assertNotNull(merged2, "Merged value should not be null");
598+
599+
CVMLong mergedTime2 = DLFSNode.getUTime(merged2);
600+
assertEquals(2000L, mergedTime2.longValue(), "Merged root should use max node timestamp when context timestamp is null");
601+
602+
// Test 3: Basic merge (no context) should behave as before
603+
AVector<ACell> merged3 = lattice.merge(node1, node2);
604+
assertNotNull(merged3, "Merged value should not be null");
605+
606+
CVMLong mergedTime3 = DLFSNode.getUTime(merged3);
607+
assertEquals(2000L, mergedTime3.longValue(), "Basic merge should use max node timestamp");
608+
609+
// Test 4: Verify DirectoryEntriesLattice passes context through
610+
// Create two filesystems with subdirectories
611+
DLFileSystem fs3 = DLFS.createLocal();
612+
fs3.setTimestamp(CVMLong.create(1000));
613+
Path dir1 = Files.createDirectory(fs3.getPath("subdir"));
614+
Path file3 = Files.createFile(dir1.resolve("file.txt"));
615+
try (OutputStream os = Files.newOutputStream(file3)) {
616+
os.write(new byte[] {7, 8, 9});
617+
}
618+
AVector<ACell> node3 = fs3.getNode(fs3.getRoot());
619+
620+
DLFileSystem fs4 = DLFS.createLocal();
621+
fs4.setTimestamp(CVMLong.create(2000));
622+
Path dir2 = Files.createDirectory(fs4.getPath("subdir"));
623+
Path file4 = Files.createFile(dir2.resolve("other.txt"));
624+
try (OutputStream os = Files.newOutputStream(file4)) {
625+
os.write(new byte[] {10, 11, 12});
626+
}
627+
AVector<ACell> node4 = fs4.getNode(fs4.getRoot());
628+
629+
// Merge with context timestamp = 4000
630+
LatticeContext context2 = LatticeContext.create(CVMLong.create(4000), null);
631+
AVector<ACell> merged4 = lattice.merge(context2, node3, node4);
632+
633+
// Verify merged root uses context timestamp
634+
assertEquals(4000L, DLFSNode.getUTime(merged4).longValue(), "Merged root with subdirs should use context timestamp");
635+
636+
// Verify subdirectory also gets context timestamp during merge
637+
AVector<ACell> mergedSubdir = DLFSNode.navigate(merged4, fs3.getPath("subdir"));
638+
assertNotNull(mergedSubdir, "Merged subdirectory should exist");
639+
assertEquals(4000L, DLFSNode.getUTime(mergedSubdir).longValue(), "Merged subdirectory should use context timestamp");
640+
}
641+
550642
}

convex-core/src/test/java/convex/lattice/generic/GenericLatticeTest.java

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,4 +91,73 @@ public void testSignedLatticeContextFallback() {
9191
assertEquals(true, result.checkSignature());
9292
}
9393

94+
@Test
95+
public void testOwnerLatticeWithContext() {
96+
// Create two key pairs for two different owners
97+
AKeyPair kp1 = AKeyPair.generate();
98+
AKeyPair kp2 = AKeyPair.generate();
99+
CVMLong ts = CVMLong.create(System.currentTimeMillis());
100+
101+
// Create OwnerLattice for signed max values
102+
OwnerLattice<AInteger> ownerLattice = OwnerLattice.create(MaxLattice.create());
103+
104+
// Create first owner's signed values
105+
ACell owner1 = kp1.getAccountKey();
106+
SignedData<AInteger> owner1Val1 = kp1.signData(CVMLong.create(10));
107+
AHashMap<ACell, SignedData<AInteger>> map1 = Maps.of(owner1, owner1Val1);
108+
109+
// Create second owner's signed values
110+
ACell owner2 = kp2.getAccountKey();
111+
SignedData<AInteger> owner2Val1 = kp2.signData(CVMLong.create(20));
112+
AHashMap<ACell, SignedData<AInteger>> map2 = Maps.of(owner2, owner2Val1);
113+
114+
// Merge two different owners - should combine both entries
115+
LatticeContext ctx1 = LatticeContext.create(ts, kp1);
116+
AHashMap<ACell, SignedData<AInteger>> merged = ownerLattice.merge(ctx1, map1, map2);
117+
118+
assertEquals(2, merged.size(), "Merged map should have both owners");
119+
assertEquals(CVMLong.create(10), merged.get(owner1).getValue(), "Owner1's value should be preserved");
120+
assertEquals(CVMLong.create(20), merged.get(owner2).getValue(), "Owner2's value should be preserved");
121+
122+
// Update owner1's value and merge again
123+
SignedData<AInteger> owner1Val2 = kp1.signData(CVMLong.create(30));
124+
AHashMap<ACell, SignedData<AInteger>> map3 = Maps.of(owner1, owner1Val2);
125+
126+
LatticeContext ctx2 = LatticeContext.create(ts, kp1);
127+
AHashMap<ACell, SignedData<AInteger>> merged2 = ownerLattice.merge(ctx2, merged, map3);
128+
129+
assertEquals(2, merged2.size(), "Merged map should still have both owners");
130+
assertEquals(CVMLong.create(30), merged2.get(owner1).getValue(), "Owner1's value should be updated to max (30)");
131+
assertEquals(CVMLong.create(20), merged2.get(owner2).getValue(), "Owner2's value should remain unchanged");
132+
133+
// Verify signatures are valid
134+
assertEquals(true, merged2.get(owner1).checkSignature(), "Owner1's signature should be valid");
135+
assertEquals(true, merged2.get(owner2).checkSignature(), "Owner2's signature should be valid");
136+
}
137+
138+
@Test
139+
public void testOwnerLatticeContextFallback() {
140+
// Test that OwnerLattice works without context (backwards compatibility)
141+
AKeyPair kp1 = AKeyPair.generate();
142+
AKeyPair kp2 = AKeyPair.generate();
143+
144+
OwnerLattice<AInteger> ownerLattice = OwnerLattice.create(MaxLattice.create());
145+
146+
// Create owner values
147+
ACell owner1 = kp1.getAccountKey();
148+
SignedData<AInteger> owner1Val = kp1.signData(CVMLong.create(10));
149+
AHashMap<ACell, SignedData<AInteger>> map1 = Maps.of(owner1, owner1Val);
150+
151+
ACell owner2 = kp2.getAccountKey();
152+
SignedData<AInteger> owner2Val = kp2.signData(CVMLong.create(20));
153+
AHashMap<ACell, SignedData<AInteger>> map2 = Maps.of(owner2, owner2Val);
154+
155+
// Merge without context (basic merge)
156+
AHashMap<ACell, SignedData<AInteger>> merged = ownerLattice.merge(map1, map2);
157+
158+
assertEquals(2, merged.size(), "Merged map should have both owners");
159+
assertEquals(CVMLong.create(10), merged.get(owner1).getValue());
160+
assertEquals(CVMLong.create(20), merged.get(owner2).getValue());
161+
}
162+
94163
}

0 commit comments

Comments
 (0)