cassandra-trunk/test/distributed/org/apache/cassandra/distributed/test/EphemeralSnapshotTest.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.cassandra.distributed.test;

import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;

import com.google.common.util.concurrent.Futures;
import org.junit.Test;

import org.apache.cassandra.db.Keyspace;
import org.apache.cassandra.distributed.Cluster;
import org.apache.cassandra.distributed.api.IInvokableInstance;
import org.apache.cassandra.distributed.api.IIsolatedExecutor;
import org.apache.cassandra.io.util.File;
import org.apache.cassandra.service.snapshot.SnapshotManifest;
import org.apache.cassandra.utils.Pair;

import static java.lang.String.format;
import static java.util.concurrent.TimeUnit.SECONDS;
import static org.apache.cassandra.distributed.api.ConsistencyLevel.ONE;
import static org.apache.cassandra.distributed.api.Feature.GOSSIP;
import static org.apache.cassandra.distributed.api.Feature.NATIVE_PROTOCOL;
import static org.apache.cassandra.distributed.api.Feature.NETWORK;
import static org.awaitility.Awaitility.await;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;

public class EphemeralSnapshotTest extends TestBaseImpl
{
    private static final String snapshotName = "snapshotname";
    private static final String snapshotName2 = "second-snapshot";
    private static final String tableName = "city";

    @Test
    public void testStartupRemovesEphemeralSnapshotOnEphemeralFlagInManifest() throws Exception
    {
        try (Cluster c = init(builder().withNodes(1)
                                       .withConfig(config -> config.with(GOSSIP, NETWORK, NATIVE_PROTOCOL))
                                       .start()))
        {
            Pair<String, String[]> initialisationData = initialise(c);

            rewriteManifestToEphemeral(initialisationData.left, initialisationData.right);

            verify(c.get(1));
        }
    }

    // TODO this test might be deleted once we get rid of ephemeral marker file for good in 4.3
    @Test
    public void testStartupRemovesEphemeralSnapshotOnMarkerFile() throws Exception
    {
        try (Cluster c = init(builder().withNodes(1)
                                       .withConfig(config -> config.with(GOSSIP, NETWORK, NATIVE_PROTOCOL))
                                       .start()))
        {
            Pair<String, String[]> initialisationData = initialise(c);

            String tableId = initialisationData.left;
            String[] dataDirs = initialisationData.right;

            // place ephemeral marker file into snapshot directory pretending it was created as ephemeral
            Path ephemeralMarkerFile = Paths.get(dataDirs[0])
                                            .resolve(KEYSPACE)
                                            .resolve(format("%s-%s", tableName, tableId))
                                            .resolve("snapshots")
                                            .resolve(snapshotName)
                                            .resolve("ephemeral.snapshot");

            Files.createFile(ephemeralMarkerFile);

            verify(c.get(1));
        }
    }

    @Test
    public void testEphemeralSnapshotIsNotClearableFromNodetool() throws Exception
    {
        try (Cluster c = init(builder().withNodes(1)
                                       .withConfig(config -> config.with(GOSSIP, NETWORK, NATIVE_PROTOCOL))
                                       .start()))
        {
            IInvokableInstance instance = c.get(1);

            Pair<String, String[]> initialisationData = initialise(c);
            rewriteManifestToEphemeral(initialisationData.left, initialisationData.right);

            assertTrue(instance.nodetoolResult("listsnapshots", "-e").getStdout().contains(snapshotName));
            instance.nodetoolResult("clearsnapshot", "-t", snapshotName).asserts().success();
            // ephemeral snapshot was not removed as it can not be (from nodetool / user operation)
            assertTrue(instance.nodetoolResult("listsnapshots", "-e").getStdout().contains(snapshotName));

            assertFalse(instance.logs().grep("Skipping deletion of ephemeral snapshot 'snapshotname' in keyspace distributed_test_keyspace. " +
                                             "Ephemeral snapshots are not removable by a user.").getResult().isEmpty());
        }
    }

    @Test
    public void testClearingAllSnapshotsFromNodetoolWillKeepEphemeralSnaphotsIntact() throws Exception
    {
        try (Cluster c = init(builder().withNodes(1)
                                       .withConfig(config -> config.with(GOSSIP, NETWORK, NATIVE_PROTOCOL))
                                       .start()))
        {
            IInvokableInstance instance = c.get(1);

            Pair<String, String[]> initialisationData = initialise(c);

            rewriteManifestToEphemeral(initialisationData.left, initialisationData.right);

            instance.nodetoolResult("clearsnapshot", "--all").asserts().success();
            assertTrue(instance.nodetoolResult("listsnapshots", "-e").getStdout().contains(snapshotName));
            assertFalse(instance.nodetoolResult("listsnapshots", "-e").getStdout().contains(snapshotName2));
        }
    }

    private Pair<String, String[]> initialise(Cluster c)
    {
        c.schemaChange(withKeyspace("CREATE TABLE IF NOT EXISTS %s." + tableName + " (cityid int PRIMARY KEY, name text)"));
        c.coordinator(1).execute(withKeyspace("INSERT INTO %s." + tableName + "(cityid, name) VALUES (1, 'Canberra');"), ONE);
        IInvokableInstance instance = c.get(1);

        instance.flush(KEYSPACE);

        assertEquals(0, instance.nodetool("snapshot", "-kt", withKeyspace("%s." + tableName), "-t", snapshotName));
        waitForSnapshot(instance, snapshotName);

        // take one more snapshot, this one is not ephemeral,
        // starting Cassandra will clear ephemerals, but it will not affect non-ephemeral snapshots
        assertEquals(0, instance.nodetool("snapshot", "-kt", withKeyspace("%s." + tableName), "-t", snapshotName2));
        waitForSnapshot(instance, snapshotName2);

        String tableId = instance.callOnInstance((IIsolatedExecutor.SerializableCallable<String>) () -> {
            return Keyspace.open(KEYSPACE).getMetadata().tables.get(tableName).get().id.asUUID().toString().replaceAll("-", "");
        });

        String[] dataDirs = (String[]) instance.config().get("data_file_directories");

        return Pair.create(tableId, dataDirs);
    }


    private void verify(IInvokableInstance instance)
    {
        // by default, we do not see ephemerals
        assertFalse(instance.nodetoolResult("listsnapshots").getStdout().contains(snapshotName));

        // we see them via -e flag
        assertTrue(instance.nodetoolResult("listsnapshots", "-e").getStdout().contains(snapshotName));

        Futures.getUnchecked(instance.shutdown());

        // startup should remove ephemeral snapshot
        instance.startup();

        assertFalse(instance.nodetoolResult("listsnapshots", "-e").getStdout().contains(snapshotName));
        assertTrue(instance.nodetoolResult("listsnapshots", "-e").getStdout().contains(snapshotName2));
    }

    private void waitForSnapshot(IInvokableInstance instance, String snapshotName)
    {
        await().timeout(20, SECONDS)
               .pollInterval(1, SECONDS)
               .until(() -> instance.nodetoolResult("listsnapshots", "-e").getStdout().contains(snapshotName));
    }

    private void rewriteManifestToEphemeral(String tableId, String[] dataDirs) throws Exception
    {
        // rewrite manifest, pretend that it is ephemeral
        Path manifestPath = findManifest(dataDirs, tableId);
        SnapshotManifest manifest = SnapshotManifest.deserializeFromJsonFile(new File(manifestPath));
        SnapshotManifest manifestWithEphemeralFlag = new SnapshotManifest(manifest.files, null, manifest.createdAt, true);
        manifestWithEphemeralFlag.serializeToJsonFile(new File(manifestPath));
    }

    private Path findManifest(String[] dataDirs, String tableId)
    {
        for (String dataDir : dataDirs)
        {
            Path manifest = Paths.get(dataDir)
                                 .resolve(KEYSPACE)
                                 .resolve(format("%s-%s", tableName, tableId))
                                 .resolve("snapshots")
                                 .resolve(snapshotName)
                                 .resolve("manifest.json");

            if (Files.exists(manifest))
            {
                return manifest;
            }
        }

        throw new IllegalStateException("Unable to find manifest!");
    }
}
