From 6b1bd1745ab9f04f9e379e6d22764b97693ba2ad Mon Sep 17 00:00:00 2001 From: Tyler Hobbs Date: Thu, 11 Feb 2016 10:26:47 -0600 Subject: [PATCH] Make SELECT JSON and toJson() threadsafe Patch by Ivan Ryndin and Tyler Hobbs; reviewed by Sergio Bossa for CASSANDRA-11048 --- CHANGES.txt | 1 + src/java/org/apache/cassandra/cql3/Json.java | 10 ++- .../cassandra/cql3/selection/Selection.java | 2 +- .../cassandra/db/marshal/AsciiType.java | 2 +- .../apache/cassandra/db/marshal/MapType.java | 2 +- .../apache/cassandra/db/marshal/UTF8Type.java | 2 +- .../apache/cassandra/db/marshal/UserType.java | 2 +- .../cql3/validation/entities/JsonTest.java | 70 ++++++++++++++++--- 8 files changed, 76 insertions(+), 15 deletions(-) diff --git a/CHANGES.txt b/CHANGES.txt index 5674b9d57d..3aca7205c5 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -1,4 +1,5 @@ 2.2.6 + * Make SELECT JSON and toJson() threadsafe (CASSANDRA-11048) * Fix SELECT on tuple relations for mixed ASC/DESC clustering order (CASSANDRA-7281) * (cqlsh) Support utf-8/cp65001 encoding on Windows (CASSANDRA-11030) * Fix paging on DISTINCT queries repeats result when first row in partition changes (CASSANDRA-10010) diff --git a/src/java/org/apache/cassandra/cql3/Json.java b/src/java/org/apache/cassandra/cql3/Json.java index 5284793e80..78fbd084a3 100644 --- a/src/java/org/apache/cassandra/cql3/Json.java +++ b/src/java/org/apache/cassandra/cql3/Json.java @@ -35,10 +35,16 @@ public class Json { public static final ObjectMapper JSON_OBJECT_MAPPER = new ObjectMapper(); - public static final JsonStringEncoder JSON_STRING_ENCODER = new JsonStringEncoder(); - public static final ColumnIdentifier JSON_COLUMN_ID = new ColumnIdentifier("[json]", true); + /** + * Quotes string contents using standard JSON quoting. + */ + public static String quoteAsJsonString(String s) + { + return new String(JsonStringEncoder.getInstance().quoteAsString(s)); + } + public static Object decodeJson(String json) { try diff --git a/src/java/org/apache/cassandra/cql3/selection/Selection.java b/src/java/org/apache/cassandra/cql3/selection/Selection.java index f6925b2779..6016059911 100644 --- a/src/java/org/apache/cassandra/cql3/selection/Selection.java +++ b/src/java/org/apache/cassandra/cql3/selection/Selection.java @@ -375,7 +375,7 @@ public abstract class Selection ByteBuffer buffer = row.get(i); sb.append('"'); - sb.append(Json.JSON_STRING_ENCODER.quoteAsString(columnName)); + sb.append(Json.quoteAsJsonString(columnName)); sb.append("\": "); if (buffer == null) sb.append("null"); diff --git a/src/java/org/apache/cassandra/db/marshal/AsciiType.java b/src/java/org/apache/cassandra/db/marshal/AsciiType.java index 953fc0925a..2356c1c4ed 100644 --- a/src/java/org/apache/cassandra/db/marshal/AsciiType.java +++ b/src/java/org/apache/cassandra/db/marshal/AsciiType.java @@ -88,7 +88,7 @@ public class AsciiType extends AbstractType { try { - return '"' + new String(Json.JSON_STRING_ENCODER.quoteAsString(ByteBufferUtil.string(buffer, Charset.forName("US-ASCII")))) + '"'; + return '"' + Json.quoteAsJsonString(ByteBufferUtil.string(buffer, Charset.forName("US-ASCII"))) + '"'; } catch (CharacterCodingException exc) { diff --git a/src/java/org/apache/cassandra/db/marshal/MapType.java b/src/java/org/apache/cassandra/db/marshal/MapType.java index 22f4df1b2a..434702af16 100644 --- a/src/java/org/apache/cassandra/db/marshal/MapType.java +++ b/src/java/org/apache/cassandra/db/marshal/MapType.java @@ -244,7 +244,7 @@ public class MapType extends CollectionType> if (key.startsWith("\"")) sb.append(key); else - sb.append('"').append(Json.JSON_STRING_ENCODER.quoteAsString(key)).append('"'); + sb.append('"').append(Json.quoteAsJsonString(key)).append('"'); sb.append(": "); sb.append(values.toJSONString(CollectionSerializer.readValue(buffer, protocolVersion), protocolVersion)); diff --git a/src/java/org/apache/cassandra/db/marshal/UTF8Type.java b/src/java/org/apache/cassandra/db/marshal/UTF8Type.java index 3fd175ccb4..3b93d9a7eb 100644 --- a/src/java/org/apache/cassandra/db/marshal/UTF8Type.java +++ b/src/java/org/apache/cassandra/db/marshal/UTF8Type.java @@ -67,7 +67,7 @@ public class UTF8Type extends AbstractType { try { - return '"' + new String(Json.JSON_STRING_ENCODER.quoteAsString(ByteBufferUtil.string(buffer, Charset.forName("UTF-8")))) + '"'; + return '"' + Json.quoteAsJsonString(ByteBufferUtil.string(buffer, Charset.forName("UTF-8"))) + '"'; } catch (CharacterCodingException exc) { diff --git a/src/java/org/apache/cassandra/db/marshal/UserType.java b/src/java/org/apache/cassandra/db/marshal/UserType.java index 23e279055a..40a35b5615 100644 --- a/src/java/org/apache/cassandra/db/marshal/UserType.java +++ b/src/java/org/apache/cassandra/db/marshal/UserType.java @@ -206,7 +206,7 @@ public class UserType extends TupleType name = "\"" + name + "\""; sb.append('"'); - sb.append(Json.JSON_STRING_ENCODER.quoteAsString(name)); + sb.append(Json.quoteAsJsonString(name)); sb.append("\": "); ByteBuffer valueBuffer = buffers[i]; diff --git a/test/unit/org/apache/cassandra/cql3/validation/entities/JsonTest.java b/test/unit/org/apache/cassandra/cql3/validation/entities/JsonTest.java index 7f8fa0bb91..57df1e687f 100644 --- a/test/unit/org/apache/cassandra/cql3/validation/entities/JsonTest.java +++ b/test/unit/org/apache/cassandra/cql3/validation/entities/JsonTest.java @@ -25,6 +25,7 @@ import org.apache.cassandra.serializers.SimpleDateSerializer; import org.apache.cassandra.serializers.TimeSerializer; import org.apache.cassandra.utils.ByteBufferUtil; +import org.junit.Assert; import org.junit.BeforeClass; import org.junit.Test; @@ -32,11 +33,12 @@ import java.math.BigDecimal; import java.math.BigInteger; import java.net.InetAddress; import java.text.SimpleDateFormat; -import java.util.Date; -import java.util.UUID; +import java.util.*; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; public class JsonTest extends CQLTester @@ -927,11 +929,11 @@ public class JsonTest extends CQLTester // map, text> keys String innerKey1 = "[\"0\", \"1\"]"; - String fullKey1 = String.format("{\"%s\": \"%s\"}", new String(Json.JSON_STRING_ENCODER.quoteAsString(innerKey1)), "a"); - String stringKey1 = new String(Json.JSON_STRING_ENCODER.quoteAsString(fullKey1)); + String fullKey1 = String.format("{\"%s\": \"%s\"}", Json.quoteAsJsonString(innerKey1), "a"); + String stringKey1 = Json.quoteAsJsonString(fullKey1); String innerKey2 = "[\"3\", \"4\"]"; - String fullKey2 = String.format("{\"%s\": \"%s\"}", new String(Json.JSON_STRING_ENCODER.quoteAsString(innerKey2)), "b"); - String stringKey2 = new String(Json.JSON_STRING_ENCODER.quoteAsString(fullKey2)); + String fullKey2 = String.format("{\"%s\": \"%s\"}", Json.quoteAsJsonString(innerKey2), "b"); + String stringKey2 = Json.quoteAsJsonString(fullKey2); execute("INSERT INTO %s JSON ?", "{\"k\": 0, \"nestedsetmap\": {\"" + stringKey1 + "\": true, \"" + stringKey2 + "\": false}}"); assertRows(execute("SELECT JSON k, nestedsetmap FROM %s"), row("{\"k\": 0, \"nestedsetmap\": {\"" + stringKey1 + "\": true, \"" + stringKey2 + "\": false}}")); @@ -955,4 +957,56 @@ public class JsonTest extends CQLTester execute("INSERT INTO %s JSON ?", "{\"k\": 0, \"a\": {\"a\": 0, \"b\": [1, 2, 3], \"c\": null}, \"b\": null}"); assertRows(execute("SELECT k, a.a, a.b, a.c, b FROM %s"), row(0, 0, set(1, 2, 3), null, null)); } + + // done for CASSANDRA-11048 + @Test + public void testJsonTreadSafety() throws Throwable + { + int numThreads = 10; + final int numRows = 10000; + + createTable("CREATE TABLE %s (" + + "k text PRIMARY KEY, " + + "v text)"); + + for (int i = 0; i < numRows; i++) + execute("INSERT INTO %s (k, v) VALUES (?, ?)", "" + i, "" + i); + + long seed = System.nanoTime(); + System.out.println("Seed " + seed); + final Random rand = new Random(seed); + + final Runnable worker = new Runnable() + { + @Override + public void run() + { + try + { + for (int i = 0; i < numRows; i++) + { + String key = "" + rand.nextInt(numRows); + assertRows(execute("SELECT JSON * FROM %s WHERE k = ?", key), + row(String.format("{\"k\": \"%s\", \"v\": \"%s\"}", key, key))); + } + } + catch (Throwable exc) + { + exc.printStackTrace(); + fail(exc.getMessage()); + } + } + }; + + ExecutorService executor = Executors.newFixedThreadPool(numThreads); + List futures = new ArrayList<>(); + for (int i = 0; i < numThreads; i++) + futures.add(executor.submit(worker)); + + for (Future future : futures) + future.get(10, TimeUnit.SECONDS); + + executor.shutdown(); + Assert.assertTrue(executor.awaitTermination(10, TimeUnit.SECONDS)); + } }