4141import org .junit .Test ;
4242
4343import java .io .IOException ;
44- import java .lang .reflect .Field ;
4544import java .nio .ByteBuffer ;
4645import java .nio .ByteOrder ;
47- import java .nio .charset .StandardCharsets ;
4846import java .nio .file .Paths ;
4947import java .util .ArrayList ;
5048import java .util .Arrays ;
@@ -1099,20 +1097,25 @@ public void testRecoverySeedKeepsUtf8CollidingSymbolsInLockstep() throws Excepti
10991097 }
11001098 Assert .assertEquals ("two colliding symbols must persist as two entries" , 2 , persistedSize );
11011099
1102- // Phase 2: recover. The seeded producer id space must match pd.size().
1100+ // Phase 2: recover, then introduce a third symbol. The wire must extend
1101+ // the two recovered ids at id 2 rather than overwrite id 1.
11031102 DictReconstructingHandler handler = new DictReconstructingHandler ();
11041103 try (TestWebSocketServer good = new TestWebSocketServer (handler )) {
11051104 int port = good .getPort ();
11061105 good .start ();
11071106 Assert .assertTrue (good .awaitStart (5 , TimeUnit .SECONDS ));
11081107 String cfg = "ws::addr=localhost:" + port + ";sf_dir=" + sfDir + ";" ;
11091108 try (Sender s2 = Sender .fromConfig (cfg )) {
1110- Assert .assertEquals ("recovered producer dictionary must match the persisted "
1111- + "entry count (not de-duped), else the delta baseline desyncs from the mirror" ,
1112- persistedSize , globalDictSize (s2 ));
1113- Assert .assertEquals ("delta baseline must resume at the persisted tip" ,
1114- persistedSize - 1 , intField (s2 , "sentMaxSymbolId" ));
1109+ s2 .table ("m" ).symbol ("s" , "tail" ).longColumn ("v" , 3L ).atNow ();
1110+ long fsn = s2 .flushAndGetSequence ();
1111+ Assert .assertTrue ("the post-recovery frame must be acknowledged" ,
1112+ s2 .awaitAckedFsn (fsn , 5_000 ));
11151113 }
1114+ Assert .assertEquals ("the new symbol delta must continue after both recovered entries" ,
1115+ persistedSize , handler .lastDataDeltaStart ());
1116+ Assert .assertEquals ("the reconstructed dictionary must retain both colliding entries "
1117+ + "and append the new symbol at id 2" ,
1118+ Arrays .asList ("?" , "?" , "tail" ), handler .dictSnapshot ());
11161119 }
11171120 });
11181121 }
@@ -1369,19 +1372,6 @@ public void testFullDictFramesRecoverInFullDictModeInsteadOfBricking() throws Ex
13691372 });
13701373 }
13711374
1372- private static int globalDictSize (Sender sender ) throws Exception {
1373- Field f = sender .getClass ().getDeclaredField ("globalSymbolDictionary" );
1374- f .setAccessible (true );
1375- Object dict = f .get (sender );
1376- return (int ) dict .getClass ().getMethod ("size" ).invoke (dict );
1377- }
1378-
1379- private static int intField (Sender sender , String name ) throws Exception {
1380- Field f = sender .getClass ().getDeclaredField (name );
1381- f .setAccessible (true );
1382- return f .getInt (sender );
1383- }
1384-
13851375 private static void writeAckWatermark (java .nio .file .Path path , long fsn ) throws IOException {
13861376 byte [] buf = new byte [16 ];
13871377 ByteBuffer bb = ByteBuffer .wrap (buf ).order (ByteOrder .LITTLE_ENDIAN );
@@ -1391,23 +1381,6 @@ private static void writeAckWatermark(java.nio.file.Path path, long fsn) throws
13911381 java .nio .file .Files .write (path , buf );
13921382 }
13931383
1394- private static int readVarint (byte [] buf , int [] pos ) {
1395- int result = 0 ;
1396- int shift = 0 ;
1397- while (pos [0 ] < buf .length ) {
1398- int b = buf [pos [0 ]++] & 0xFF ;
1399- result |= (b & 0x7F ) << shift ;
1400- if ((b & 0x80 ) == 0 ) {
1401- return result ;
1402- }
1403- shift += 7 ;
1404- if (shift > 28 ) {
1405- throw new IllegalStateException ("varint too long" );
1406- }
1407- }
1408- throw new IllegalStateException ("varint truncated" );
1409- }
1410-
14111384 /**
14121385 * Reconstructs the per-connection symbol dictionary from delta sections,
14131386 * mirroring the server's {@code setQuick(deltaStart + i)} + null-padding.
@@ -1418,6 +1391,7 @@ private static class DictReconstructingHandler implements TestWebSocketServer.We
14181391 private final List <String > dict = new ArrayList <>();
14191392 private final AtomicLong nextSeq = new AtomicLong (0 );
14201393 private TestWebSocketServer .ClientHandler currentClient ;
1394+ private volatile int lastDataDeltaStart = -1 ;
14211395
14221396 synchronized List <Long > ackSequenceStarts () {
14231397 return new ArrayList <>(ackSequenceStarts );
@@ -1431,6 +1405,10 @@ synchronized int maxDictSize() {
14311405 return dict .size ();
14321406 }
14331407
1408+ int lastDataDeltaStart () {
1409+ return lastDataDeltaStart ;
1410+ }
1411+
14341412 @ Override
14351413 public synchronized void onBinaryMessage (TestWebSocketServer .ClientHandler client , byte [] data ) {
14361414 boolean newConnection = currentClient != client ;
@@ -1439,59 +1417,23 @@ public synchronized void onBinaryMessage(TestWebSocketServer.ClientHandler clien
14391417 dict .clear (); // fresh server dictionary per connection
14401418 nextSeq .set (0 );
14411419 }
1442- accumulate (data );
1443- if (tableCount (data ) == 0 && hasDelta (data )) {
1420+ QwpWireTestUtils .accumulateDeltaDictionary (data , dict );
1421+ int tableCount = QwpWireTestUtils .tableCount (data );
1422+ if (tableCount == 0 && QwpWireTestUtils .hasDelta (data )) {
14441423 sawCatchUpFrame = true ;
1424+ } else if (tableCount > 0 && QwpWireTestUtils .hasDelta (data )) {
1425+ lastDataDeltaStart = QwpWireTestUtils .readVarint (data , new int []{12 });
14451426 }
14461427 try {
14471428 long ackSequence = nextSeq .getAndIncrement ();
14481429 if (newConnection ) {
14491430 ackSequenceStarts .add (ackSequence );
14501431 }
1451- client .sendBinary (buildAck (ackSequence ));
1432+ client .sendBinary (QwpWireTestUtils . buildAck (ackSequence ));
14521433 } catch (IOException e ) {
14531434 throw new RuntimeException (e );
14541435 }
14551436 }
1456-
1457- private static byte [] buildAck (long seq ) {
1458- byte [] buf = new byte [1 + 8 + 2 ];
1459- ByteBuffer bb = ByteBuffer .wrap (buf ).order (ByteOrder .LITTLE_ENDIAN );
1460- bb .put ((byte ) 0x00 );
1461- bb .putLong (seq );
1462- bb .putShort ((short ) 0 );
1463- return buf ;
1464- }
1465-
1466- private static boolean hasDelta (byte [] frame ) {
1467- return frame .length >= 12 && (frame [5 ] & 0x08 ) != 0 ;
1468- }
1469-
1470- private static int tableCount (byte [] frame ) {
1471- return (frame [6 ] & 0xFF ) | ((frame [7 ] & 0xFF ) << 8 );
1472- }
1473-
1474- private void accumulate (byte [] frame ) {
1475- if (!hasDelta (frame )) {
1476- return ;
1477- }
1478- int [] pos = {12 };
1479- int deltaStart = readVarint (frame , pos );
1480- int deltaCount = readVarint (frame , pos );
1481- while (dict .size () < deltaStart ) {
1482- dict .add (null );
1483- }
1484- for (int i = 0 ; i < deltaCount ; i ++) {
1485- int len = readVarint (frame , pos );
1486- String sym = new String (frame , pos [0 ], len , StandardCharsets .UTF_8 );
1487- pos [0 ] += len ;
1488- int idx = deltaStart + i ;
1489- while (dict .size () <= idx ) {
1490- dict .add (null );
1491- }
1492- dict .set (idx , sym );
1493- }
1494- }
14951437 }
14961438
14971439 private static class SilentHandler implements TestWebSocketServer .WebSocketServerHandler {
@@ -1510,20 +1452,11 @@ private static class CountingHandler implements TestWebSocketServer.WebSocketSer
15101452 public void onBinaryMessage (TestWebSocketServer .ClientHandler client , byte [] data ) {
15111453 frames .incrementAndGet ();
15121454 try {
1513- client .sendBinary (buildAck (nextSeq .getAndIncrement ()));
1455+ client .sendBinary (QwpWireTestUtils . buildAck (nextSeq .getAndIncrement ()));
15141456 } catch (IOException e ) {
15151457 throw new RuntimeException (e );
15161458 }
15171459 }
1518-
1519- private static byte [] buildAck (long seq ) {
1520- byte [] buf = new byte [1 + 8 + 2 ];
1521- ByteBuffer bb = ByteBuffer .wrap (buf ).order (ByteOrder .LITTLE_ENDIAN );
1522- bb .put ((byte ) 0x00 );
1523- bb .putLong (seq );
1524- bb .putShort ((short ) 0 );
1525- return buf ;
1526- }
15271460 }
15281461 /**
15291462 * Short-writes the FIRST dictionary append after it is armed, modelling a disk that
0 commit comments