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 -&gt; (ACL_1, COUNT: 1),
324      *      and data tree N1 -&gt; ID1
325      *   4. On server A, it will be empty ACL map, and N1 -&gt; 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