/* * 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.brooklyn.entity.nosql.cassandra; import static com.google.common.base.Preconditions.checkNotNull; import static org.testng.Assert.assertEquals; import static org.testng.Assert.assertNull; import static org.testng.Assert.assertTrue; import org.apache.brooklyn.core.entity.Attributes; import org.apache.brooklyn.entity.nosql.cassandra.CassandraNode; import org.apache.brooklyn.util.exceptions.Exceptions; import org.apache.brooklyn.util.text.Identifiers; import org.apache.brooklyn.util.time.Duration; import org.apache.brooklyn.util.time.Time; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.testng.Assert; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.netflix.astyanax.AstyanaxContext; import com.netflix.astyanax.Cluster; import com.netflix.astyanax.Keyspace; import com.netflix.astyanax.MutationBatch; import com.netflix.astyanax.connectionpool.NodeDiscoveryType; import com.netflix.astyanax.connectionpool.OperationResult; import com.netflix.astyanax.connectionpool.exceptions.ConnectionException; import com.netflix.astyanax.connectionpool.exceptions.SchemaDisagreementException; import com.netflix.astyanax.connectionpool.impl.ConnectionPoolConfigurationImpl; import com.netflix.astyanax.connectionpool.impl.CountingConnectionPoolMonitor; import com.netflix.astyanax.impl.AstyanaxConfigurationImpl; import com.netflix.astyanax.model.Column; import com.netflix.astyanax.model.ColumnFamily; import com.netflix.astyanax.model.ColumnList; import com.netflix.astyanax.serializers.StringSerializer; import com.netflix.astyanax.thrift.ThriftFamilyFactory; /** * Cassandra testing using Astyanax API. */ public class AstyanaxSupport { private static final Logger log = LoggerFactory.getLogger(AstyanaxSupport.class); public final String clusterName; public final String hostname; public final int thriftPort; public AstyanaxSupport(CassandraNode node) { this(node.getClusterName(), node.getAttribute(Attributes.HOSTNAME), node.getThriftPort()); } public AstyanaxSupport(String clusterName, String hostname, int thriftPort) { this.clusterName = clusterName; this.hostname = hostname; this.thriftPort = thriftPort; } public AstyanaxContext<Keyspace> newAstyanaxContextForKeyspace(String keyspace) { AstyanaxContext<Keyspace> context = new AstyanaxContext.Builder() .forCluster(clusterName) .forKeyspace(keyspace) .withAstyanaxConfiguration(new AstyanaxConfigurationImpl() .setDiscoveryType(NodeDiscoveryType.NONE)) .withConnectionPoolConfiguration(new ConnectionPoolConfigurationImpl("BrooklynPool") .setPort(thriftPort) .setMaxConnsPerHost(1) .setConnectTimeout(5000) // 10s .setSeeds(String.format("%s:%d", hostname, thriftPort))) .withConnectionPoolMonitor(new CountingConnectionPoolMonitor()) .buildKeyspace(ThriftFamilyFactory.getInstance()); context.start(); return context; } public AstyanaxContext<Cluster> newAstyanaxContextForCluster() { AstyanaxContext<Cluster> context = new AstyanaxContext.Builder() .forCluster(clusterName) .withAstyanaxConfiguration(new AstyanaxConfigurationImpl() .setDiscoveryType(NodeDiscoveryType.NONE)) .withConnectionPoolConfiguration(new ConnectionPoolConfigurationImpl("BrooklynPool") .setPort(thriftPort) .setMaxConnsPerHost(1) .setConnectTimeout(5000) // 10s .setSeeds(String.format("%s:%d", hostname, thriftPort))) .withConnectionPoolMonitor(new CountingConnectionPoolMonitor()) .buildCluster(ThriftFamilyFactory.getInstance()); context.start(); return context; } public static class AstyanaxSample extends AstyanaxSupport { public static class Builder { protected CassandraNode node; protected String clusterName; protected String hostname; protected Integer thriftPort; protected String columnFamilyName = Identifiers.makeRandomId(8); public Builder node(CassandraNode val) { this.node = val; clusterName = node.getClusterName(); hostname = node.getAttribute(Attributes.HOSTNAME); thriftPort = node.getThriftPort(); return this; } public Builder host(String clusterName, String hostname, int thriftPort) { this.clusterName = clusterName; this.hostname = hostname; this.thriftPort = thriftPort; return this; } public Builder columnFamilyName(String val) { this.columnFamilyName = val; return this; } public AstyanaxSample build() { return new AstyanaxSample(this); } } public static Builder builder() { return new Builder(); } public final String columnFamilyName; public final ColumnFamily<String, String> sampleColumnFamily; public AstyanaxSample(CassandraNode node) { this(builder().node(node)); } public AstyanaxSample(String clusterName, String hostname, int thriftPort) { this(builder().host(clusterName, hostname, thriftPort)); } protected AstyanaxSample(Builder builder) { super(builder.clusterName, builder.hostname, builder.thriftPort); columnFamilyName = checkNotNull(builder.columnFamilyName, "columnFamilyName"); sampleColumnFamily = new ColumnFamily<String, String>( columnFamilyName, // Column Family Name StringSerializer.get(), // Key Serializer StringSerializer.get()); // Column Serializer } /** * Exercise the {@link CassandraNode} using the Astyanax API. */ public void astyanaxTest() throws Exception { String keyspaceName = "BrooklynTests_"+Identifiers.makeRandomId(8); writeData(keyspaceName); readData(keyspaceName); } /** * Write to a {@link CassandraNode} using the Astyanax API. * @throws ConnectionException */ public void writeData(String keyspaceName) throws ConnectionException { // Create context AstyanaxContext<Keyspace> context = newAstyanaxContextForKeyspace(keyspaceName); try { Keyspace keyspace = context.getEntity(); try { checkNull(keyspace.describeKeyspace().getColumnFamily(columnFamilyName), "key space for column family "+columnFamilyName); } catch (Exception ek) { // (Re) Create keyspace if needed (including if family name already existed, // e.g. due to a timeout on previous attempt) log.debug("repairing Cassandra error by re-creating keyspace "+keyspace+": "+ek); try { log.debug("dropping Cassandra keyspace "+keyspace); keyspace.dropKeyspace(); } catch (Exception e) { /* Ignore */ log.debug("Cassandra keyspace "+keyspace+" could not be dropped (probably did not exist): "+e); } try { keyspace.createKeyspace(ImmutableMap.<String, Object>builder() .put("strategy_options", ImmutableMap.<String, Object>of("replication_factor", "1")) .put("strategy_class", "SimpleStrategy") .build()); } catch (SchemaDisagreementException e) { // discussion (but not terribly helpful) at http://stackoverflow.com/questions/6770894/schemadisagreementexception // let's just try again after a delay // (seems to have no effect; trying to fix by starting first node before others) log.warn("error creating Cassandra keyspace "+keyspace+" (retrying): "+e); Time.sleep(Duration.FIVE_SECONDS); keyspace.createKeyspace(ImmutableMap.<String, Object>builder() .put("strategy_options", ImmutableMap.<String, Object>of("replication_factor", "1")) .put("strategy_class", "SimpleStrategy") .build()); } } assertNull(keyspace.describeKeyspace().getColumnFamily("Rabbits"), "key space for arbitrary column family Rabbits"); assertNull(keyspace.describeKeyspace().getColumnFamily(columnFamilyName), "key space for column family "+columnFamilyName); // Create column family keyspace.createColumnFamily(sampleColumnFamily, null); // Insert rows MutationBatch m = keyspace.prepareMutationBatch(); m.withRow(sampleColumnFamily, "one") .putColumn("name", "Alice", null) .putColumn("company", "Cloudsoft Corp", null); m.withRow(sampleColumnFamily, "two") .putColumn("name", "Bob", null) .putColumn("company", "Cloudsoft Corp", null) .putColumn("pet", "Cat", null); OperationResult<Void> insert = m.execute(); assertEquals(insert.getHost().getHostName(), hostname); assertTrue(insert.getLatency() > 0L); } finally { context.shutdown(); } } /** * Read from a {@link CassandraNode} using the Astyanax API. * @throws ConnectionException */ public void readData(String keyspaceName) throws ConnectionException { // Create context AstyanaxContext<Keyspace> context = newAstyanaxContextForKeyspace(keyspaceName); try { Keyspace keyspace = context.getEntity(); // Query data OperationResult<ColumnList<String>> query = keyspace.prepareQuery(sampleColumnFamily) .getKey("one") .execute(); assertEquals(query.getHost().getHostName(), hostname); assertTrue(query.getLatency() > 0L); ColumnList<String> columns = query.getResult(); assertEquals(columns.size(), 2); // Lookup columns in response by name String name = columns.getColumnByName("name").getStringValue(); assertEquals(name, "Alice"); // Iterate through the columns for (Column<String> c : columns) { assertTrue(ImmutableList.of("name", "company").contains(c.getName())); } } finally { context.shutdown(); } } /** * Returns the keyspace name to which the data has been written. If it fails the first time, * then will increment the keyspace name. This is because the failure could be a response timeout, * where the keyspace really has been created so subsequent attempts with the same name will * fail (because we assert that the keyspace did not exist). */ public String writeData(String keyspacePrefix, int numRetries) throws ConnectionException { int retryCount = 0; while (true) { try { String keyspaceName = keyspacePrefix + (retryCount > 0 ? "" : "_"+retryCount); writeData(keyspaceName); return keyspaceName; } catch (Exception e) { log.warn("Error writing data - attempt "+(retryCount+1)+" of "+(numRetries+1)+": "+e, e); if (++retryCount > numRetries) throw Exceptions.propagate(e); } } } /** * Repeatedly tries to read data from the given keyspace name. Asserts that the data is the * same as would be written by calling {@code writeData(keyspaceName)}. */ public void readData(String keyspaceName, int numRetries) throws ConnectionException { int retryCount = 0; while (true) { try { readData(keyspaceName); return; } catch (Exception e) { log.warn("Error reading data - attempt "+(retryCount+1)+" of "+(numRetries+1)+": "+e, e); if (++retryCount > numRetries) throw Exceptions.propagate(e); } } } /** * Like {@link Assert#assertNull(Object, String)}, except throws IllegalStateException instead */ private void checkNull(Object obj, String msg) { if (obj != null) { throw new IllegalStateException("Not null: "+msg+"; obj="+obj); } } } public static void main(String[] args) throws Exception { AstyanaxSample support = new AstyanaxSample("ignored", "ec2-79-125-32-2.eu-west-1.compute.amazonaws.com", 9160); AstyanaxContext<Cluster> context = support.newAstyanaxContextForCluster(); try { System.out.println(context.getEntity().describeSchemaVersions()); } finally { context.shutdown(); } } }