1 /* 2 * Licensed to the Apache Software Foundation (ASF) under one 3 * or more contributor license agreements. See the NOTICE file 4 * distributed with this work for additional information 5 * regarding copyright ownership. The ASF licenses this file 6 * to you under the Apache License, Version 2.0 (the 7 * "License"); you may not use this file except in compliance 8 * with the License. You may obtain a copy of the License at 9 * 10 * http://www.apache.org/licenses/LICENSE-2.0 11 * 12 * Unless required by applicable law or agreed to in writing, software 13 * distributed under the License is distributed on an "AS IS" BASIS, 14 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 * See the License for the specific language governing permissions and 16 * limitations under the License. 17 */ 18 19 package org.apache.zookeeper.server.persistence; 20 21 import static org.junit.jupiter.api.Assertions.assertEquals; 22 import static org.junit.jupiter.api.Assertions.assertFalse; 23 import static org.junit.jupiter.api.Assertions.assertNotEquals; 24 import static org.junit.jupiter.api.Assertions.assertNotNull; 25 import static org.junit.jupiter.api.Assertions.assertNull; 26 import static org.junit.jupiter.api.Assertions.assertThrows; 27 import static org.junit.jupiter.api.Assertions.assertTrue; 28 import static org.junit.jupiter.api.Assertions.fail; 29 import java.io.File; 30 import java.io.FileInputStream; 31 import java.io.FileOutputStream; 32 import java.io.IOException; 33 import java.util.Map; 34 import java.util.concurrent.ConcurrentHashMap; 35 import org.apache.jute.BinaryInputArchive; 36 import org.apache.jute.BinaryOutputArchive; 37 import org.apache.jute.InputArchive; 38 import org.apache.jute.OutputArchive; 39 import org.apache.jute.Record; 40 import org.apache.zookeeper.ZooDefs; 41 import org.apache.zookeeper.server.DataNode; 42 import org.apache.zookeeper.server.DataTree; 43 import org.apache.zookeeper.server.Request; 44 import org.apache.zookeeper.server.ZooKeeperServer; 45 import org.apache.zookeeper.test.ClientBase; 46 import org.apache.zookeeper.test.TestUtils; 47 import org.apache.zookeeper.txn.CreateTxn; 48 import org.apache.zookeeper.txn.SetDataTxn; 49 import org.apache.zookeeper.txn.TxnDigest; 50 import org.apache.zookeeper.txn.TxnHeader; 51 import org.junit.jupiter.api.AfterEach; 52 import org.junit.jupiter.api.BeforeEach; 53 import org.junit.jupiter.api.Test; 54 55 public class FileTxnSnapLogTest { 56 57 private File tmpDir; 58 59 private File logDir; 60 61 private File snapDir; 62 63 private File logVersionDir; 64 65 private File snapVersionDir; 66 67 @BeforeEach setUp()68 public void setUp() throws Exception { 69 tmpDir = ClientBase.createEmptyTestDir(); 70 logDir = new File(tmpDir, "logdir"); 71 snapDir = new File(tmpDir, "snapdir"); 72 } 73 74 @AfterEach tearDown()75 public void tearDown() throws Exception { 76 if (tmpDir != null) { 77 TestUtils.deleteFileRecursively(tmpDir); 78 } 79 this.tmpDir = null; 80 this.logDir = null; 81 this.snapDir = null; 82 this.logVersionDir = null; 83 this.snapVersionDir = null; 84 } 85 createVersionDir(File parentDir)86 private File createVersionDir(File parentDir) { 87 File versionDir = new File(parentDir, FileTxnSnapLog.version + FileTxnSnapLog.VERSION); 88 versionDir.mkdirs(); 89 return versionDir; 90 } 91 createLogFile(File dir, long zxid)92 private void createLogFile(File dir, long zxid) throws IOException { 93 File file = new File(dir.getPath() + File.separator + Util.makeLogName(zxid)); 94 file.createNewFile(); 95 } 96 createSnapshotFile(File dir, long zxid)97 private void createSnapshotFile(File dir, long zxid) throws IOException { 98 File file = new File(dir.getPath() + File.separator + Util.makeSnapshotName(zxid)); 99 file.createNewFile(); 100 } 101 twoDirSetupWithCorrectFiles()102 private void twoDirSetupWithCorrectFiles() throws IOException { 103 logVersionDir = createVersionDir(logDir); 104 snapVersionDir = createVersionDir(snapDir); 105 106 // transaction log files in log dir 107 createLogFile(logVersionDir, 1); 108 createLogFile(logVersionDir, 2); 109 110 // snapshot files in snap dir 111 createSnapshotFile(snapVersionDir, 1); 112 createSnapshotFile(snapVersionDir, 2); 113 } 114 singleDirSetupWithCorrectFiles()115 private void singleDirSetupWithCorrectFiles() throws IOException { 116 logVersionDir = createVersionDir(logDir); 117 118 // transaction log and snapshot files in the same dir 119 createLogFile(logVersionDir, 1); 120 createLogFile(logVersionDir, 2); 121 createSnapshotFile(logVersionDir, 1); 122 createSnapshotFile(logVersionDir, 2); 123 } 124 createFileTxnSnapLogWithNoAutoCreateDataDir(File logDir, File snapDir)125 private FileTxnSnapLog createFileTxnSnapLogWithNoAutoCreateDataDir(File logDir, File snapDir) throws IOException { 126 return createFileTxnSnapLogWithAutoCreateDataDir(logDir, snapDir, "false"); 127 } 128 createFileTxnSnapLogWithAutoCreateDataDir( File logDir, File snapDir, String autoCreateValue)129 private FileTxnSnapLog createFileTxnSnapLogWithAutoCreateDataDir( 130 File logDir, 131 File snapDir, 132 String autoCreateValue) throws IOException { 133 String priorAutocreateDirValue = System.getProperty(FileTxnSnapLog.ZOOKEEPER_DATADIR_AUTOCREATE); 134 System.setProperty(FileTxnSnapLog.ZOOKEEPER_DATADIR_AUTOCREATE, autoCreateValue); 135 FileTxnSnapLog fileTxnSnapLog; 136 try { 137 fileTxnSnapLog = new FileTxnSnapLog(logDir, snapDir); 138 } finally { 139 if (priorAutocreateDirValue == null) { 140 System.clearProperty(FileTxnSnapLog.ZOOKEEPER_DATADIR_AUTOCREATE); 141 } else { 142 System.setProperty(FileTxnSnapLog.ZOOKEEPER_DATADIR_AUTOCREATE, priorAutocreateDirValue); 143 } 144 } 145 return fileTxnSnapLog; 146 } 147 createFileTxnSnapLogWithAutoCreateDB( File logDir, File snapDir, String autoCreateValue)148 private FileTxnSnapLog createFileTxnSnapLogWithAutoCreateDB( 149 File logDir, 150 File snapDir, 151 String autoCreateValue) throws IOException { 152 String priorAutocreateDBValue = System.getProperty(FileTxnSnapLog.ZOOKEEPER_DB_AUTOCREATE); 153 System.setProperty(FileTxnSnapLog.ZOOKEEPER_DB_AUTOCREATE, autoCreateValue); 154 FileTxnSnapLog fileTxnSnapLog; 155 try { 156 fileTxnSnapLog = new FileTxnSnapLog(logDir, snapDir); 157 } finally { 158 if (priorAutocreateDBValue == null) { 159 System.clearProperty(FileTxnSnapLog.ZOOKEEPER_DB_AUTOCREATE); 160 } else { 161 System.setProperty(FileTxnSnapLog.ZOOKEEPER_DB_AUTOCREATE, priorAutocreateDBValue); 162 } 163 } 164 return fileTxnSnapLog; 165 } 166 167 /** 168 * Test verifies the auto creation of log dir and snap dir. 169 * Sets "zookeeper.datadir.autocreate" to true. 170 */ 171 @Test testWithAutoCreateDataDir()172 public void testWithAutoCreateDataDir() throws IOException { 173 assertFalse(logDir.exists(), "log directory already exists"); 174 assertFalse(snapDir.exists(), "snapshot directory already exists"); 175 176 FileTxnSnapLog fileTxnSnapLog = createFileTxnSnapLogWithAutoCreateDataDir(logDir, snapDir, "true"); 177 178 assertTrue(logDir.exists()); 179 assertTrue(snapDir.exists()); 180 assertTrue(fileTxnSnapLog.getDataDir().exists()); 181 assertTrue(fileTxnSnapLog.getSnapDir().exists()); 182 } 183 184 /** 185 * Test verifies server should fail when log dir or snap dir doesn't exist. 186 * Sets "zookeeper.datadir.autocreate" to false. 187 */ 188 @Test testWithoutAutoCreateDataDir()189 public void testWithoutAutoCreateDataDir() throws Exception { 190 assertThrows(FileTxnSnapLog.DatadirException.class, () -> { 191 assertFalse(logDir.exists(), "log directory already exists"); 192 assertFalse(snapDir.exists(), "snapshot directory already exists"); 193 194 try { 195 createFileTxnSnapLogWithAutoCreateDataDir(logDir, snapDir, "false"); 196 } catch (FileTxnSnapLog.DatadirException e) { 197 assertFalse(logDir.exists()); 198 assertFalse(snapDir.exists()); 199 // rethrow exception 200 throw e; 201 } 202 fail("Expected exception from FileTxnSnapLog"); 203 }); 204 } 205 attemptAutoCreateDB( File dataDir, File snapDir, Map<Long, Integer> sessions, String autoCreateValue, long expectedValue)206 private void attemptAutoCreateDB( 207 File dataDir, 208 File snapDir, 209 Map<Long, Integer> sessions, 210 String autoCreateValue, 211 long expectedValue) throws IOException { 212 sessions.clear(); 213 214 FileTxnSnapLog fileTxnSnapLog = createFileTxnSnapLogWithAutoCreateDB(dataDir, snapDir, autoCreateValue); 215 216 long zxid = fileTxnSnapLog.restore(new DataTree(), sessions, new FileTxnSnapLog.PlayBackListener() { 217 @Override 218 public void onTxnLoaded(TxnHeader hdr, Record rec, TxnDigest digest) { 219 // empty by default 220 } 221 }); 222 assertEquals(expectedValue, zxid, "unexpected zxid"); 223 } 224 225 @Test testAutoCreateDB()226 public void testAutoCreateDB() throws IOException { 227 assertTrue(logDir.mkdir(), "cannot create log directory"); 228 assertTrue(snapDir.mkdir(), "cannot create snapshot directory"); 229 File initFile = new File(logDir, "initialize"); 230 assertFalse(initFile.exists(), "initialize file already exists"); 231 232 Map<Long, Integer> sessions = new ConcurrentHashMap<>(); 233 234 attemptAutoCreateDB(logDir, snapDir, sessions, "false", -1L); 235 attemptAutoCreateDB(logDir, snapDir, sessions, "true", 0L); 236 237 assertTrue(initFile.createNewFile(), "cannot create initialize file"); 238 attemptAutoCreateDB(logDir, snapDir, sessions, "false", 0L); 239 } 240 241 @Test testGetTxnLogSyncElapsedTime()242 public void testGetTxnLogSyncElapsedTime() throws IOException { 243 FileTxnSnapLog fileTxnSnapLog = createFileTxnSnapLogWithAutoCreateDataDir(logDir, snapDir, "true"); 244 245 TxnHeader hdr = new TxnHeader(1, 1, 1, 1, ZooDefs.OpCode.setData); 246 Record txn = new SetDataTxn("/foo", new byte[0], 1); 247 Request req = new Request(0, 0, 0, hdr, txn, 0); 248 249 try { 250 fileTxnSnapLog.append(req); 251 fileTxnSnapLog.commit(); 252 long syncElapsedTime = fileTxnSnapLog.getTxnLogElapsedSyncTime(); 253 assertNotEquals(-1L, syncElapsedTime, "Did not update syncElapsedTime!"); 254 } finally { 255 fileTxnSnapLog.close(); 256 } 257 } 258 259 @Test testDirCheckWithCorrectFiles()260 public void testDirCheckWithCorrectFiles() throws IOException { 261 twoDirSetupWithCorrectFiles(); 262 263 try { 264 createFileTxnSnapLogWithNoAutoCreateDataDir(logDir, snapDir); 265 } catch (FileTxnSnapLog.LogDirContentCheckException | FileTxnSnapLog.SnapDirContentCheckException e) { 266 fail("Should not throw ContentCheckException."); 267 } 268 } 269 270 @Test testDirCheckWithSingleDirSetup()271 public void testDirCheckWithSingleDirSetup() throws IOException { 272 singleDirSetupWithCorrectFiles(); 273 274 try { 275 createFileTxnSnapLogWithNoAutoCreateDataDir(logDir, logDir); 276 } catch (FileTxnSnapLog.LogDirContentCheckException | FileTxnSnapLog.SnapDirContentCheckException e) { 277 fail("Should not throw ContentCheckException."); 278 } 279 } 280 281 @Test testDirCheckWithSnapFilesInLogDir()282 public void testDirCheckWithSnapFilesInLogDir() throws IOException { 283 assertThrows(FileTxnSnapLog.LogDirContentCheckException.class, () -> { 284 twoDirSetupWithCorrectFiles(); 285 286 // add snapshot files to the log version dir 287 createSnapshotFile(logVersionDir, 3); 288 createSnapshotFile(logVersionDir, 4); 289 290 createFileTxnSnapLogWithNoAutoCreateDataDir(logDir, snapDir); 291 }); 292 } 293 294 @Test testDirCheckWithLogFilesInSnapDir()295 public void testDirCheckWithLogFilesInSnapDir() throws IOException { 296 assertThrows(FileTxnSnapLog.SnapDirContentCheckException.class, () -> { 297 twoDirSetupWithCorrectFiles(); 298 299 // add transaction log files to the snap version dir 300 createLogFile(snapVersionDir, 3); 301 createLogFile(snapVersionDir, 4); 302 303 createFileTxnSnapLogWithNoAutoCreateDataDir(logDir, snapDir); 304 }); 305 } 306 307 /** 308 * Make sure the ACL is exist in the ACL map after SNAP syncing. 309 * 310 * ZooKeeper uses ACL reference id and count to save the space in snapshot. 311 * During fuzzy snapshot sync, the reference count may not be updated 312 * correctly in case like the znode is already exist. 313 * 314 * When ACL reference count reaches 0, it will be deleted from the cache, 315 * but actually there might be other nodes still using it. When visiting 316 * a node with the deleted ACL id, it will be rejected because it doesn't 317 * exist anymore. 318 * 319 * Here is the detailed flow for one of the scenario here: 320 * 1. Server A starts to have snap sync with leader 321 * 2. After serializing the ACL map to Server A, there is a txn T1 to 322 * create a node N1 with new ACL_1 which was not exist in ACL map 323 * 3. On leader, after this txn, the ACL map will be ID1 -> (ACL_1, COUNT: 1), 324 * and data tree N1 -> ID1 325 * 4. On server A, it will be empty ACL map, and N1 -> ID1 in fuzzy snapshot 326 * 5. When replaying the txn T1, it will skip at the beginning since the 327 * node is already exist, which leaves an empty ACL map, and N1 is 328 * referencing to a non-exist ACL ID1 329 * 6. Node N1 will be not accessible because the ACL not exist, and if it 330 * became leader later then all the write requests will be rejected as 331 * well with marshalling error. 332 */ 333 @Test testACLCreatedDuringFuzzySnapshotSync()334 public void testACLCreatedDuringFuzzySnapshotSync() throws IOException { 335 DataTree leaderDataTree = new DataTree(); 336 337 // Start the simulated snap-sync by serializing ACL cache. 338 File file = File.createTempFile("snapshot", "zk"); 339 FileOutputStream os = new FileOutputStream(file); 340 OutputArchive oa = BinaryOutputArchive.getArchive(os); 341 leaderDataTree.serializeAcls(oa); 342 343 // Add couple of transaction in-between. 344 TxnHeader hdr1 = new TxnHeader(1, 2, 2, 2, ZooDefs.OpCode.create); 345 Record txn1 = new CreateTxn("/a1", "foo".getBytes(), ZooDefs.Ids.CREATOR_ALL_ACL, false, -1); 346 leaderDataTree.processTxn(hdr1, txn1); 347 348 // Finish the snapshot. 349 leaderDataTree.serializeNodes(oa); 350 os.close(); 351 352 // Simulate restore on follower and replay. 353 FileInputStream is = new FileInputStream(file); 354 InputArchive ia = BinaryInputArchive.getArchive(is); 355 DataTree followerDataTree = new DataTree(); 356 followerDataTree.deserialize(ia, "tree"); 357 followerDataTree.processTxn(hdr1, txn1); 358 359 DataNode a1 = leaderDataTree.getNode("/a1"); 360 assertNotNull(a1); 361 assertEquals(ZooDefs.Ids.CREATOR_ALL_ACL, leaderDataTree.getACL(a1)); 362 363 assertEquals(ZooDefs.Ids.CREATOR_ALL_ACL, followerDataTree.getACL(a1)); 364 } 365 366 @Test testEmptySnapshotSerialization()367 public void testEmptySnapshotSerialization() throws IOException { 368 File dataDir = ClientBase.createEmptyTestDir(); 369 FileTxnSnapLog snaplog = new FileTxnSnapLog(dataDir, dataDir); 370 DataTree dataTree = new DataTree(); 371 ConcurrentHashMap<Long, Integer> sessions = new ConcurrentHashMap<>(); 372 373 ZooKeeperServer.setDigestEnabled(true); 374 snaplog.save(dataTree, sessions, true); 375 snaplog.restore(dataTree, sessions, (hdr, rec, digest) -> { }); 376 377 assertNull(dataTree.getDigestFromLoadedSnapshot()); 378 } 379 380 @Test testSnapshotSerializationCompatibility()381 public void testSnapshotSerializationCompatibility() throws IOException { 382 testSnapshotSerializationCompatibility(true, false); 383 testSnapshotSerializationCompatibility(false, false); 384 testSnapshotSerializationCompatibility(true, true); 385 testSnapshotSerializationCompatibility(false, true); 386 } 387 testSnapshotSerializationCompatibility(Boolean digestEnabled, Boolean snappyEnabled)388 void testSnapshotSerializationCompatibility(Boolean digestEnabled, Boolean snappyEnabled) throws IOException { 389 File dataDir = ClientBase.createEmptyTestDir(); 390 FileTxnSnapLog snaplog = new FileTxnSnapLog(dataDir, dataDir); 391 DataTree dataTree = new DataTree(); 392 ConcurrentHashMap<Long, Integer> sessions = new ConcurrentHashMap<>(); 393 SnapStream.setStreamMode(snappyEnabled ? SnapStream.StreamMode.SNAPPY : SnapStream.StreamMode.DEFAULT_MODE); 394 395 ZooKeeperServer.setDigestEnabled(digestEnabled); 396 TxnHeader txnHeader = new TxnHeader(1, 1, 1, 1 + 1, ZooDefs.OpCode.create); 397 CreateTxn txn = new CreateTxn("/" + 1, "data".getBytes(), null, false, 1); 398 Request request = new Request(1, 1, 1, txnHeader, txn, 1); 399 dataTree.processTxn(request.getHdr(), request.getTxn()); 400 snaplog.save(dataTree, sessions, true); 401 402 int expectedNodeCount = dataTree.getNodeCount(); 403 ZooKeeperServer.setDigestEnabled(!digestEnabled); 404 snaplog.restore(dataTree, sessions, (hdr, rec, digest) -> { }); 405 assertEquals(expectedNodeCount, dataTree.getNodeCount()); 406 } 407 } 408