diff --git a/CHANGES.txt b/CHANGES.txt index e723aba8c1..9be4f3803e 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -9,7 +9,8 @@ * Hadoop integration is incompatible with Cassandra Driver 3.0.0 (CASSANDRA-11001) * Add dropped_columns to the list of schema table so it gets handled properly (CASSANDRA-11050) -Merged from 2.2.6: +Merged from 2.2: + * 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 26e7ff2216..a0b226a52f 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 4fec2bc8eb..4518f02950 100644 --- a/src/java/org/apache/cassandra/cql3/selection/Selection.java +++ b/src/java/org/apache/cassandra/cql3/selection/Selection.java @@ -386,7 +386,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 d03fc3083b..69b2b01d8c 100644 --- a/src/java/org/apache/cassandra/db/marshal/AsciiType.java +++ b/src/java/org/apache/cassandra/db/marshal/AsciiType.java @@ -83,7 +83,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 8fd5da23f9..c8a42f467d 100644 --- a/src/java/org/apache/cassandra/db/marshal/MapType.java +++ b/src/java/org/apache/cassandra/db/marshal/MapType.java @@ -245,7 +245,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 0778b0408a..7c18ce5a80 100644 --- a/src/java/org/apache/cassandra/db/marshal/UTF8Type.java +++ b/src/java/org/apache/cassandra/db/marshal/UTF8Type.java @@ -62,7 +62,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 7a0e8c832e..7e69b6342e 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)); + } }