SnapshotDigestTest.java

/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.apache.zookeeper.server;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNull;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ConcurrentHashMap;
import org.apache.zookeeper.CreateMode;
import org.apache.zookeeper.Op;
import org.apache.zookeeper.ZooDefs;
import org.apache.zookeeper.ZooKeeper;
import org.apache.zookeeper.ZooKeeper.States;
import org.apache.zookeeper.server.metric.SimpleCounter;
import org.apache.zookeeper.server.persistence.FileTxnSnapLog;
import org.apache.zookeeper.server.quorum.QuorumPeerMainTest;
import org.apache.zookeeper.test.ClientBase;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class SnapshotDigestTest extends ClientBase {

    private static final Logger LOG = LoggerFactory.getLogger(SnapshotDigestTest.class);

    private ZooKeeper zk;
    private ZooKeeperServer server;

    @BeforeEach
    public void setUp() throws Exception {
        super.setUp();
        server = serverFactory.getZooKeeperServer();
        zk = createClient();
    }

    @AfterEach
    public void tearDown() throws Exception {
        // server will be closed in super.tearDown
        super.tearDown();

        if (zk != null) {
            zk.close();
        }
    }

    @Override
    public void setupCustomizedEnv() {
        ZooKeeperServer.setDigestEnabled(true);
        System.setProperty(ZooKeeperServer.SNAP_COUNT, "100");
    }

    @Override
    public void cleanUpCustomizedEnv() {
        ZooKeeperServer.setDigestEnabled(false);
        System.clearProperty(ZooKeeperServer.SNAP_COUNT);
    }

    /**
     * Check snapshot digests when loading a fuzzy or non-fuzzy snapshot.
     */
    @Test
    public void testSnapshotDigest() throws Exception {
        // take a empty snapshot without creating any txn and make sure
        // there is no digest mismatch issue
        server.takeSnapshot();
        reloadSnapshotAndCheckDigest();

        // trigger various write requests
        String pathPrefix = "/testSnapshotDigest";
        for (int i = 0; i < 1000; i++) {
            String path = pathPrefix + i;
            zk.create(path, path.getBytes(), ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);
        }

        // update the data of first node
        String firstNode = pathPrefix + 0;
        zk.setData(firstNode, "new_setdata".getBytes(), -1);

        // delete the first node
        zk.delete(firstNode, -1);

        // trigger multi op
        List<Op> subTxns = new ArrayList<>();
        for (int i = 0; i < 3; i++) {
            String path = pathPrefix + "-m" + i;
            subTxns.add(Op.create(path, path.getBytes(), ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT));
        }
        zk.multi(subTxns);

        reloadSnapshotAndCheckDigest();

        // Take a snapshot and test the logic when loading a non-fuzzy snapshot
        server = serverFactory.getZooKeeperServer();
        server.takeSnapshot();

        reloadSnapshotAndCheckDigest();
    }

    /**
     * Make sure the code will skip digest check when it's comparing
     * digest with different version.
     *
     * This enables us to smoonthly add new fields into digest or using
     * new digest calculation.
     */
    @Test
    public void testDifferentDigestVersion() throws Exception {
        // check the current digest version
        int currentVersion = new DigestCalculator().getDigestVersion();

        // create a node
        String path = "/testDifferentDigestVersion";
        zk.create(path, path.getBytes(), ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);

        // take a full snapshot
        server.takeSnapshot();

        //increment the digest version
        int newVersion = currentVersion + 1;
        DigestCalculator newVersionDigestCalculator = Mockito.spy(DigestCalculator.class);
        Mockito.when(newVersionDigestCalculator.getDigestVersion()).thenReturn(newVersion);
        assertEquals(newVersion, newVersionDigestCalculator.getDigestVersion());

        // using mock to return different digest value when the way we
        // calculate digest changed
        FileTxnSnapLog txnSnapLog = new FileTxnSnapLog(tmpDir, tmpDir);
        DataTree dataTree = Mockito.spy(new DataTree(newVersionDigestCalculator));
        Mockito.when(dataTree.getTreeDigest()).thenReturn(0L);
        txnSnapLog.restore(dataTree, new ConcurrentHashMap<>(), Mockito.mock(FileTxnSnapLog.PlayBackListener.class));

        // make sure the reportDigestMismatch function is never called
        Mockito.verify(dataTree, Mockito.never()).reportDigestMismatch(Mockito.anyLong());
    }

    /**
     * Make sure it's backward compatible, and also we can rollback this
     * feature without corrupt the database.
     */
    @Test
    public void testBackwardCompatible() throws Exception {
        testCompatibleHelper(false, true);

        testCompatibleHelper(true, false);
    }

    private void testCompatibleHelper(Boolean enabledBefore, Boolean enabledAfter) throws Exception {

        ZooKeeperServer.setDigestEnabled(enabledBefore);
        ZooKeeperServer.setSerializeLastProcessedZxidEnabled(enabledBefore);

        // restart the server to cache the option change
        reloadSnapshotAndCheckDigest();

        // create a node
        String path = "/testCompatible" + "-" + enabledBefore + "-" + enabledAfter;
        zk.create(path, path.getBytes(), ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);

        // take a full snapshot
        server.takeSnapshot();

        ZooKeeperServer.setDigestEnabled(enabledAfter);
        ZooKeeperServer.setSerializeLastProcessedZxidEnabled(enabledAfter);

        reloadSnapshotAndCheckDigest();

        assertEquals(path, new String(zk.getData(path, false, null)));
    }

    private void reloadSnapshotAndCheckDigest() throws Exception {
        stopServer();
        QuorumPeerMainTest.waitForOne(zk, States.CONNECTING);

        ((SimpleCounter) ServerMetrics.getMetrics().DIGEST_MISMATCHES_COUNT).reset();

        startServer();
        QuorumPeerMainTest.waitForOne(zk, States.CONNECTED);

        server = serverFactory.getZooKeeperServer();

        // Snapshot digests always match
        assertEquals(0L, ServerMetrics.getMetrics().DIGEST_MISMATCHES_COUNT.get());

        // reset the digestFromLoadedSnapshot after comparing
        assertNull(server.getZKDatabase().getDataTree().getDigestFromLoadedSnapshot());
    }

}