io.crate.integrationtests.PartitionedTableConcurrentIntegrationTest.java Source code

Java tutorial

Introduction

Here is the source code for io.crate.integrationtests.PartitionedTableConcurrentIntegrationTest.java

Source

/*
 * Licensed to Crate under one or more contributor license agreements.
 * See the NOTICE file distributed with this work for additional
 * information regarding copyright ownership.  Crate 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.
 *
 * However, if you have executed another commercial license agreement
 * with Crate these terms will supersede the license and you may use the
 * software solely pursuant to the terms of the relevant commercial
 * agreement.
 */

package io.crate.integrationtests;

import io.crate.data.Bucket;
import io.crate.data.CollectionBucket;
import io.crate.metadata.PartitionName;
import io.crate.testing.SQLResponse;
import io.crate.testing.SQLTransportExecutor;
import io.crate.testing.UseJdbc;
import org.apache.lucene.util.BytesRef;
import org.elasticsearch.action.admin.cluster.reroute.ClusterRerouteRequestBuilder;
import org.elasticsearch.action.admin.cluster.state.ClusterStateResponse;
import org.elasticsearch.cluster.metadata.MetaData;
import org.elasticsearch.cluster.node.DiscoveryNode;
import org.elasticsearch.cluster.node.DiscoveryNodes;
import org.elasticsearch.cluster.routing.ShardRouting;
import org.elasticsearch.cluster.routing.ShardRoutingState;
import org.elasticsearch.cluster.routing.allocation.command.MoveAllocationCommand;
import org.elasticsearch.cluster.service.ClusterService;
import org.elasticsearch.common.Priority;
import org.elasticsearch.common.unit.TimeValue;
import org.elasticsearch.test.ESIntegTestCase;
import org.junit.After;
import org.junit.Test;

import java.util.*;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;

import static org.hamcrest.core.Is.is;

@ESIntegTestCase.ClusterScope(numDataNodes = 2)
@UseJdbc
public class PartitionedTableConcurrentIntegrationTest extends SQLTransportIntegrationTest {

    private final TimeValue ACCEPTABLE_RELOCATION_TIME = new TimeValue(10, TimeUnit.SECONDS);

    @After
    public void resetSettings() throws Exception {
        execute("reset global stats.enabled");
    }

    /**
     * Test depends on 2 data nodes
     */
    @Test
    public void testSelectWhileShardsAreRelocating() throws Throwable {
        execute("create table t (name string, p string) " + "clustered into 2 shards "
                + "partitioned by (p) with (number_of_replicas = 0)");
        ensureYellow();

        execute("insert into t (name, p) values (?, ?)",
                new Object[][] { new Object[] { "Marvin", "a" }, new Object[] { "Trillian", "a" }, });
        execute("refresh table t");
        execute("set global stats.enabled=true");

        final AtomicReference<Throwable> lastThrowable = new AtomicReference<>();
        final CountDownLatch selects = new CountDownLatch(100);

        Thread t = new Thread(new Runnable() {
            @Override
            public void run() {
                while (selects.getCount() > 0) {
                    try {
                        execute("select * from t");
                    } catch (Throwable t) {
                        // The failed job should have three started operations
                        SQLResponse res = execute(
                                "select id from sys.jobs_log where error is not null order by started desc limit 1");
                        if (res.rowCount() > 0) {
                            String id = (String) res.rows()[0][0];
                            res = execute(
                                    "select count(*) from sys.operations_log where name=? or name = ?and job_id = ?",
                                    new Object[] { "collect", "fetchContext", id });
                            if ((long) res.rows()[0][0] < 3) {
                                // set the error if there where less than three attempts
                                lastThrowable.set(t);
                            }
                        }
                    } finally {
                        selects.countDown();
                    }
                }
            }
        });
        t.start();

        PartitionName partitionName = new PartitionName("t", Collections.singletonList(new BytesRef("a")));
        final String indexName = partitionName.asIndexName();

        ClusterService clusterService = internalCluster().getInstance(ClusterService.class);
        DiscoveryNodes nodes = clusterService.state().nodes();
        List<String> nodeIds = new ArrayList<>(2);
        for (DiscoveryNode node : nodes) {
            if (node.isDataNode()) {
                nodeIds.add(node.getId());
            }
        }
        final Map<String, String> nodeSwap = new HashMap<>(2);
        nodeSwap.put(nodeIds.get(0), nodeIds.get(1));
        nodeSwap.put(nodeIds.get(1), nodeIds.get(0));

        final CountDownLatch relocations = new CountDownLatch(20);
        Thread relocatingThread = new Thread(new Runnable() {
            @Override
            public void run() {
                while (relocations.getCount() > 0) {
                    ClusterStateResponse clusterStateResponse = admin().cluster().prepareState()
                            .setIndices(indexName).execute().actionGet();
                    List<ShardRouting> shardRoutings = clusterStateResponse.getState().routingTable()
                            .allShards(indexName);

                    ClusterRerouteRequestBuilder clusterRerouteRequestBuilder = admin().cluster().prepareReroute();
                    int numMoves = 0;
                    for (ShardRouting shardRouting : shardRoutings) {
                        if (shardRouting.currentNodeId() == null) {
                            continue;
                        }
                        if (shardRouting.state() != ShardRoutingState.STARTED) {
                            continue;
                        }
                        String toNode = nodeSwap.get(shardRouting.currentNodeId());
                        clusterRerouteRequestBuilder.add(new MoveAllocationCommand(shardRouting.getIndexName(),
                                shardRouting.shardId().id(), shardRouting.currentNodeId(), toNode));
                        numMoves++;
                    }

                    if (numMoves > 0) {
                        clusterRerouteRequestBuilder.execute().actionGet();
                        client().admin().cluster().prepareHealth().setWaitForEvents(Priority.LANGUID)
                                .setWaitForNoRelocatingShards(false).setTimeout(ACCEPTABLE_RELOCATION_TIME)
                                .execute().actionGet();
                        relocations.countDown();
                    }
                }
            }
        });
        relocatingThread.start();
        relocations.await(SQLTransportExecutor.REQUEST_TIMEOUT.getSeconds() + 1, TimeUnit.SECONDS);
        selects.await(SQLTransportExecutor.REQUEST_TIMEOUT.getSeconds() + 1, TimeUnit.SECONDS);

        Throwable throwable = lastThrowable.get();
        if (throwable != null) {
            throw throwable;
        }

        t.join();
        relocatingThread.join();
    }

    private Bucket deletePartitionsAndExecutePlan(String stmt) throws Exception {
        execute("create table t (name string, p string) partitioned by (p)");
        execute("insert into t (name, p) values ('Arthur', 'a'), ('Trillian', 't')");
        ensureYellow();

        PlanForNode plan = plan(stmt);
        execute("delete from t");
        return new CollectionBucket(execute(plan).getResult());
    }

    @Test
    public void testTableUnknownExceptionIsNotRaisedIfPartitionsAreDeletedAfterPlan() throws Exception {
        Bucket bucket = deletePartitionsAndExecutePlan("select * from t");
        assertThat(bucket.size(), is(0));
    }

    @Test
    public void testTableUnknownExceptionIsNotRaisedIfPartitionsAreDeletedAfterPlanSingleNode() throws Exception {
        // with a sinlge node, this test leads to empty shard collectors
        internalCluster().ensureAtMostNumDataNodes(1);
        Bucket bucket = deletePartitionsAndExecutePlan("select * from t");
        assertThat(bucket.size(), is(0));
    }

    @Test
    public void testTableUnknownExceptionNotRaisedIfPartitionsDeletedAfterCountPlan() throws Exception {
        Bucket bucket = deletePartitionsAndExecutePlan("select count(*) from t");
        assertThat((Long) bucket.iterator().next().get(0), is(0L));
    }

    @Test
    public void testRefreshDuringPartitionDeletion() throws Exception {
        execute("create table t (name string, p string) partitioned by (p)");
        execute("insert into t (name, p) values ('Arthur', 'a'), ('Trillian', 't')");
        execute("refresh table t");

        PlanForNode plan = plan("refresh table t"); // create a plan in which the partitions exist
        execute("delete from t");

        // shouldn't throw an exception:
        execute(plan).getResult(); // execute now that the partitions are gone
    }

    @Test
    public void testOptimizeDuringPartitionDeletion() throws Exception {
        execute("create table t (name string, p string) partitioned by (p)");
        execute("insert into t (name, p) values ('Arthur', 'a'), ('Trillian', 't')");
        execute("refresh table t");

        PlanForNode plan = plan("optimize table t"); // create a plan in which the partitions exist
        execute("delete from t");

        // shouldn't throw an exception:
        execute(plan).getResult(); // execute now that the partitions are gone
    }

    private void deletePartitionWhileInsertingData(final boolean useBulk) throws Exception {
        execute("create table parted (id int, name string) " + "partitioned by (id) "
                + "with (number_of_replicas = 0)");
        ensureYellow();

        int numberOfDocs = 1000;
        final Object[][] bulkArgs = new Object[numberOfDocs][];
        for (int i = 0; i < numberOfDocs; i++) {
            bulkArgs[i] = new Object[] { i % 2, randomAsciiOfLength(10) };
        }

        // partition to delete
        final int idToDelete = 1;

        final AtomicReference<Exception> exceptionRef = new AtomicReference<>();
        final CountDownLatch insertLatch = new CountDownLatch(1);
        final String insertStmt = "insert into parted (id, name) values (?, ?)";
        Thread insertThread = new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    if (useBulk) {
                        execute(insertStmt, bulkArgs);
                    } else {
                        for (Object[] args : bulkArgs) {
                            execute(insertStmt, args);
                        }
                    }
                } catch (Exception t) {
                    exceptionRef.set(t);
                } finally {
                    insertLatch.countDown();
                }
            }
        });

        final CountDownLatch deleteLatch = new CountDownLatch(1);
        final String partitionName = new PartitionName("parted",
                Collections.singletonList(new BytesRef(String.valueOf(idToDelete)))).asIndexName();
        final Object[] deleteArgs = new Object[] { idToDelete };
        Thread deleteThread = new Thread(new Runnable() {
            @Override
            public void run() {
                boolean deleted = false;
                while (!deleted) {
                    try {
                        MetaData metaData = client().admin().cluster().prepareState().execute().actionGet()
                                .getState().metaData();
                        if (metaData.indices().get(partitionName) != null) {
                            execute("delete from parted where id = ?", deleteArgs);
                            deleted = true;
                        }
                    } catch (Throwable t) {
                        // ignore (mostly partition index does not exists yet)
                    }
                }
                deleteLatch.countDown();
            }
        });

        insertThread.start();
        deleteThread.start();
        deleteLatch.await(SQLTransportExecutor.REQUEST_TIMEOUT.getSeconds() + 1, TimeUnit.SECONDS);
        insertLatch.await(SQLTransportExecutor.REQUEST_TIMEOUT.getSeconds() + 1, TimeUnit.SECONDS);

        Exception exception = exceptionRef.get();
        if (exception != null) {
            throw exception;
        }

        insertThread.join();
        deleteThread.join();
    }

    @Test
    public void testDeletePartitionWhileInsertingData() throws Exception {
        deletePartitionWhileInsertingData(false);
    }

    @Test
    public void testDeletePartitionWhileBulkInsertingData() throws Exception {
        deletePartitionWhileInsertingData(true);
    }

    private static Map createColumnMap(int numCols, String prefix) {
        Map<String, Object> map = new HashMap<>(numCols);
        for (int i = 0; i < numCols; i++) {
            map.put(String.format("%s_col_%d", prefix, i), i);
        }
        return map;
    }

    @Test
    public void testInsertIntoDynamicObjectColumnAddsAllColumnsToTemplate() throws Exception {
        // regression test for issue that caused columns not being added to metadata/tableinfo of partitioned table
        // when inserting a lot of new dynamic columns to various partitions of a table
        execute("create table dyn_parted (id int, bucket string, data object(dynamic), primary key (id, bucket)) "
                + "partitioned by (bucket) " + "with (number_of_replicas = 0)");
        ensureYellow();

        int bulkSize = 10;
        int numCols = 5;
        String[] buckets = new String[] { "a", "b", "c", "d", "e", "f", "g", "h", "i", "j" };
        final CountDownLatch countDownLatch = new CountDownLatch(buckets.length);
        for (String bucket : buckets) {
            Object[][] bulkArgs = new Object[bulkSize][];
            for (int i = 0; i < bulkSize; i++) {
                bulkArgs[i] = new Object[] { i, bucket, createColumnMap(numCols, bucket) };
            }
            new Thread(() -> {
                try {
                    execute("insert into dyn_parted (id, bucket, data) values (?, ?, ?)", bulkArgs,
                            TimeValue.timeValueSeconds(10));
                } finally {
                    countDownLatch.countDown();
                }
            }).start();
        }

        countDownLatch.await();
        execute("select count(*) from information_schema.columns where table_name = 'dyn_parted'");
        assertThat(response.rows()[0][0], is(3L + numCols * buckets.length));
    }
}