|
| 1 | +package org.jgroups.tests; |
| 2 | + |
| 3 | +import org.jgroups.Address; |
| 4 | +import org.jgroups.Global; |
| 5 | +import org.jgroups.MergeView; |
| 6 | +import org.jgroups.View; |
| 7 | +import org.jgroups.ViewId; |
| 8 | +import org.jgroups.protocols.pbcast.JoinRsp; |
| 9 | +import org.jgroups.util.*; |
| 10 | +import org.testng.annotations.BeforeClass; |
| 11 | +import org.testng.annotations.Test; |
| 12 | + |
| 13 | +import java.io.IOException; |
| 14 | +import java.util.Arrays; |
| 15 | + |
| 16 | +import static org.testng.Assert.assertEquals; |
| 17 | +import static org.testng.Assert.assertTrue; |
| 18 | + |
| 19 | +/** |
| 20 | + * Tests that a {@link JoinRsp} can round-trip a {@link MergeView}. |
| 21 | + * <p/> |
| 22 | + * {@code JoinRsp.writeTo()} calls {@code view.writeTo()} polymorphically, so a MergeView also writes |
| 23 | + * its subgroups. {@code JoinRsp.readFrom()} however always creates a plain {@code new View()}, which |
| 24 | + * doesn't read the subgroups back. The leftover subgroup bytes desync the stream, and the following |
| 25 | + * {@code digest.readFrom(in, false)} reads the *number of subgroups* as the digest's member count. |
| 26 | + * Depending on how that count compares to the view size, iterating the digest (as |
| 27 | + * {@code NAKACK2.setDigest()} does) either blows up with an ArrayIndexOutOfBoundsException or |
| 28 | + * silently yields garbage seqnos. |
| 29 | + * <p/> |
| 30 | + * A coordinator sends a MergeView in a JOIN-RSP whenever it gets a JOIN-REQ from a member that is |
| 31 | + * already in its view (CoordGmsImpl.handleMembershipChange() -> GMS.getViewAndDigest()) while its |
| 32 | + * current view happens to be a MergeView. |
| 33 | + * |
| 34 | + * @author Claude |
| 35 | + */ |
| 36 | +@Test(groups=Global.FUNCTIONAL) |
| 37 | +public class JoinRspMergeViewTest { |
| 38 | + protected Address a, b, c; |
| 39 | + |
| 40 | + @BeforeClass |
| 41 | + protected void setup() { |
| 42 | + a=Util.createRandomAddress("A"); |
| 43 | + b=Util.createRandomAddress("B"); |
| 44 | + c=Util.createRandomAddress("C"); |
| 45 | + } |
| 46 | + |
| 47 | + /** Baseline: a regular view round-trips correctly */ |
| 48 | + public void testRegularView() throws Exception { |
| 49 | + View view=View.create(a, 5, a, b, c); |
| 50 | + _testRoundTrip(view); |
| 51 | + } |
| 52 | + |
| 53 | + /** 3 members, 2 subgroups: digest ends up with 2 seqno pairs but 3 members -> AIOOBE on iteration */ |
| 54 | + public void testMergeViewWithFewerSubgroupsThanMembers() throws Exception { |
| 55 | + View view=new MergeView(new ViewId(a, 5), Arrays.asList(a, b, c), |
| 56 | + Arrays.asList(View.create(a, 1, a, b), View.create(c, 1, c))); |
| 57 | + _testRoundTrip(view); |
| 58 | + } |
| 59 | + |
| 60 | + /** 2 members, 2 subgroups: no exception, but the seqnos are silently wrong */ |
| 61 | + public void testMergeViewWithSameSubgroupsAsMembers() throws Exception { |
| 62 | + View view=new MergeView(new ViewId(a, 5), Arrays.asList(a, b), |
| 63 | + Arrays.asList(View.create(a, 1, a), View.create(b, 1, b))); |
| 64 | + _testRoundTrip(view); |
| 65 | + } |
| 66 | + |
| 67 | + /** 2 members, 3 subgroups: digest ends up with more seqno pairs than members */ |
| 68 | + public void testMergeViewWithMoreSubgroupsThanMembers() throws Exception { |
| 69 | + View view=new MergeView(new ViewId(a, 5), Arrays.asList(a, b), |
| 70 | + Arrays.asList(View.create(a, 1, a), View.create(b, 1, b), |
| 71 | + View.create(a, 2, a, b))); |
| 72 | + _testRoundTrip(view); |
| 73 | + } |
| 74 | + |
| 75 | + /** |
| 76 | + * A JOIN-RSP from an unpatched coordinator (which writes the MergeView's subgroups) must be rejected |
| 77 | + * with an IOException, so that GMS.readJoinRsp() logs it and the joiner retries the JOIN, rather than |
| 78 | + * an ArrayIndexOutOfBoundsException propagating out of JChannel.connect(). |
| 79 | + */ |
| 80 | + public void testJoinRspFromUnpatchedCoord() throws Exception { |
| 81 | + View view=new MergeView(new ViewId(a, 5), Arrays.asList(a, b, c), |
| 82 | + Arrays.asList(View.create(a, 1, a, b), View.create(c, 1, c))); |
| 83 | + ByteArray buf=marshalLegacy(view, createDigest(view)); |
| 84 | + try { |
| 85 | + JoinRsp rsp=Util.streamableFromBuffer(JoinRsp::new, buf.array(), buf.getOffset(), buf.getLength()); |
| 86 | + for(Digest.Entry ignored: rsp.getDigest()) // what NAKACK2.setDigest() does |
| 87 | + ; |
| 88 | + throw new AssertionError("should have thrown an IOException, but got " + rsp); |
| 89 | + } |
| 90 | + catch(IOException ex) { |
| 91 | + System.out.printf("received expected exception: %s\n", ex); |
| 92 | + } |
| 93 | + } |
| 94 | + |
| 95 | + /** Marshals a JoinRsp the way 4.2.30 and earlier do: the view is written polymorphically, subgroups and all */ |
| 96 | + protected static ByteArray marshalLegacy(View view, Digest digest) throws Exception { |
| 97 | + ByteArrayDataOutputStream out=new ByteArrayDataOutputStream(512); |
| 98 | + out.writeByte(1); // Util.writeStreamable(): non-null marker |
| 99 | + out.writeByte(1 | 2); // JoinRsp: VIEW_PRESENT | DIGEST_PRESENT |
| 100 | + view.writeTo(out); |
| 101 | + digest.writeTo(out, false); |
| 102 | + return out.getBuffer(); |
| 103 | + } |
| 104 | + |
| 105 | + /** |
| 106 | + * Marshals a JoinRsp exactly as GMS.marshal(JoinRsp)/GMS.readJoinRsp() do, then asserts that the |
| 107 | + * digest survived the round trip and can be iterated (as NAKACK2.setDigest() does). |
| 108 | + */ |
| 109 | + protected static void _testRoundTrip(View view) throws Exception { |
| 110 | + Digest digest=createDigest(view); |
| 111 | + assertEquals(digest.capacity(), view.size()); |
| 112 | + |
| 113 | + ByteArray buf=Util.streamableToBuffer(new JoinRsp(view, digest)); |
| 114 | + JoinRsp rsp=Util.streamableFromBuffer(JoinRsp::new, buf.array(), buf.getOffset(), buf.getLength()); |
| 115 | + |
| 116 | + View rv=rsp.getView(); |
| 117 | + assert view.equals(rv) : String.format("view changed: view=%s, deserialized view=%s", view, rv); |
| 118 | + |
| 119 | + Digest new_digest=rsp.getDigest(); |
| 120 | + int count=0; |
| 121 | + for(Digest.Entry ignored: new_digest) // this is what NAKACK2.setDigest() does |
| 122 | + count++; |
| 123 | + assertEquals(count, view.size(), "digest has a different number of entries than the view has members"); |
| 124 | + assert new_digest.equals(digest) : |
| 125 | + String.format("digest changed: expected %s, but got %s", digest, new_digest); |
| 126 | + } |
| 127 | + |
| 128 | + /** Creates a digest matching the view, as CoordGmsImpl does before sending a JOIN-RSP */ |
| 129 | + protected static Digest createDigest(View view) { |
| 130 | + MutableDigest digest=new MutableDigest(view.getMembersRaw()); |
| 131 | + long seqno=10; |
| 132 | + for(Address mbr: view.getMembersRaw()) |
| 133 | + digest.set(mbr, seqno, seqno+=10); |
| 134 | + assertTrue(digest.allSet()); |
| 135 | + return digest; |
| 136 | + } |
| 137 | +} |
| 138 | + |
0 commit comments