You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@datasketches.apache.org by jm...@apache.org on 2021/07/28 20:36:18 UTC
[datasketches-server] branch test_coverage updated: finish update
coverage,
use HttpStatus class as source of status codes eveerywhere -- and actually
add the files this time
This is an automated email from the ASF dual-hosted git repository.
jmalkin pushed a commit to branch test_coverage
in repository https://gitbox.apache.org/repos/asf/datasketches-server.git
The following commit(s) were added to refs/heads/test_coverage by this push:
new c3f59da finish update coverage, use HttpStatus class as source of status codes eveerywhere -- and actually add the files this time
c3f59da is described below
commit c3f59da094e29c3cc7d562eab36894eb77d4f8cf
Author: Jon Malkin <jm...@users.noreply.github.com>
AuthorDate: Wed Jul 28 13:35:15 2021 -0700
finish update coverage, use HttpStatus class as source of status codes eveerywhere -- and actually add the files this time
---
.../server/BaseSketchesQueryHandler.java | 24 +-
.../datasketches/server/SketchConstants.java | 3 -
.../apache/datasketches/server/SketchServer.java | 1 +
.../datasketches/server/SketchServerConfig.java | 47 ++-
.../apache/datasketches/server/SketchStorage.java | 5 +-
.../apache/datasketches/server/UpdateHandler.java | 108 +++---
.../apache/datasketches/server/ServerTestBase.java | 33 +-
.../datasketches/server/SketchStorageTest.java | 4 +-
.../datasketches/server/UpdateHandlerTest.java | 383 +++++++++++++++++++--
src/test/resources/test_config.json | 134 ++++---
10 files changed, 588 insertions(+), 154 deletions(-)
diff --git a/src/main/java/org/apache/datasketches/server/BaseSketchesQueryHandler.java b/src/main/java/org/apache/datasketches/server/BaseSketchesQueryHandler.java
index 211733a..7a1bcf6 100644
--- a/src/main/java/org/apache/datasketches/server/BaseSketchesQueryHandler.java
+++ b/src/main/java/org/apache/datasketches/server/BaseSketchesQueryHandler.java
@@ -27,10 +27,11 @@ import java.io.Reader;
import java.net.URLDecoder;
import org.apache.datasketches.Family;
+import org.checkerframework.checker.nullness.qual.NonNull;
+import org.eclipse.jetty.http.HttpStatus;
import org.eclipse.jetty.server.Request;
import org.eclipse.jetty.server.handler.AbstractHandler;
-import com.google.gson.GsonBuilder;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
@@ -59,7 +60,7 @@ public abstract class BaseSketchesQueryHandler extends AbstractHandler {
* Basic query handler. Assumes calls must include a JSON query.
* @param sketches The sketches database to use
*/
- BaseSketchesQueryHandler(final SketchStorage sketches) {
+ BaseSketchesQueryHandler(@NonNull final SketchStorage sketches) {
this(sketches, false);
}
@@ -68,10 +69,7 @@ public abstract class BaseSketchesQueryHandler extends AbstractHandler {
* @param sketches The sketches database to use
* @param queryExempt <tt>true</tt> if a query is not required, otherwise <tt>false</tt>
*/
- BaseSketchesQueryHandler(final SketchStorage sketches, final boolean queryExempt) {
- if (sketches == null) {
- throw new IllegalArgumentException("Cannot initialize handler with SketchStorage == null");
- }
+ BaseSketchesQueryHandler(@NonNull final SketchStorage sketches, final boolean queryExempt) {
this.sketches = sketches;
this.queryExempt = queryExempt;
}
@@ -89,7 +87,7 @@ public abstract class BaseSketchesQueryHandler extends AbstractHandler {
response.setContentType("text/html");
query = JsonParser.parseString(URLDecoder.decode(request.getQueryString(), "utf-8"));
} else {
- response.setStatus(HttpServletResponse.SC_METHOD_NOT_ALLOWED);
+ response.setStatus(HttpStatus.METHOD_NOT_ALLOWED_405);
baseRequest.setHandled(true);
}
@@ -134,27 +132,27 @@ public abstract class BaseSketchesQueryHandler extends AbstractHandler {
}
}
} else {
- //result = callProcessQuery((JsonObject) query);
result = processQuery((JsonObject) query);
}
if (result != null) {
- //response.getWriter().print(result.toString());
- response.getWriter().print(new GsonBuilder().setPrettyPrinting().create().toJson(result));
+ response.getWriter().print(result);
+ //response.getWriter().print(new GsonBuilder().setPrettyPrinting().create().toJson(result));
}
// we're ok if we reach here without an exception
- response.setStatus(HttpServletResponse.SC_OK);
+ response.setStatus(HttpStatus.OK_200);
} catch (final Exception e) {
final JsonObject error = new JsonObject();
error.addProperty(ERROR_KEY, e.getMessage());
- response.setStatus(UNPROCESSABLE_ENTITY);
+ response.getWriter().print(error);
+ response.setStatus(HttpStatus.UNPROCESSABLE_ENTITY_422);
}
baseRequest.setHandled(true);
}
- static Family familyFromString(final String type) throws IllegalArgumentException {
+ static Family familyFromString(@NonNull final String type) throws IllegalArgumentException {
switch (type.toLowerCase()) {
case SKETCH_FAMILY_THETA:
return Family.QUICKSELECT;
diff --git a/src/main/java/org/apache/datasketches/server/SketchConstants.java b/src/main/java/org/apache/datasketches/server/SketchConstants.java
index 1e4059b..c363148 100644
--- a/src/main/java/org/apache/datasketches/server/SketchConstants.java
+++ b/src/main/java/org/apache/datasketches/server/SketchConstants.java
@@ -107,8 +107,5 @@ public final class SketchConstants {
// server configuration
public static final int DEFAULT_PORT = 8080;
- // response codes
- public static final int UNPROCESSABLE_ENTITY = 422; // defined, but not in HttpServletResponse.SC_* codes
-
public static final String ERROR_KEY = "error";
}
diff --git a/src/main/java/org/apache/datasketches/server/SketchServer.java b/src/main/java/org/apache/datasketches/server/SketchServer.java
index e0a8a0e..b693d3e 100644
--- a/src/main/java/org/apache/datasketches/server/SketchServer.java
+++ b/src/main/java/org/apache/datasketches/server/SketchServer.java
@@ -165,5 +165,6 @@ public class SketchServer {
final SketchServer sketchServer = new SketchServer(args[0]);
sketchServer.start();
+ sketchServer.server.join();
}
}
diff --git a/src/main/java/org/apache/datasketches/server/SketchServerConfig.java b/src/main/java/org/apache/datasketches/server/SketchServerConfig.java
index 447e36e..97cf391 100644
--- a/src/main/java/org/apache/datasketches/server/SketchServerConfig.java
+++ b/src/main/java/org/apache/datasketches/server/SketchServerConfig.java
@@ -19,6 +19,7 @@
package org.apache.datasketches.server;
+import static org.apache.datasketches.server.BaseSketchesQueryHandler.familyFromString;
import static org.apache.datasketches.server.SketchStorage.isDistinctCounting;
import java.io.IOException;
@@ -51,12 +52,20 @@ class SketchServerConfig {
public String family;
public String type;
- SketchInfo(final String name, final int k, final String family, final String type) {
+ SketchInfo(@NonNull final String name, final int k, @NonNull final String family, final String type) {
this.name = name;
this.k = k;
this.family = family;
this.type = type;
}
+
+ @Override
+ public String toString() {
+ return "Name : " + name + "\n" +
+ "k : " + k + "\n" +
+ "Family: " + family + "\n" +
+ "Type : " + type + "\n";
+ }
}
private int port = DEFAULT_PORT;
@@ -83,6 +92,24 @@ class SketchServerConfig {
return JsonParser.parseReader(reader);
}
+ private String validateSketchInfo() {
+ final StringBuilder sb = new StringBuilder();
+
+ for (final SketchInfo info : sketchList) {
+ if (info.name == null) {
+ sb.append("Missing sketch name: ").append(new Gson().toJson(info)).append("\n");
+ } else if (info.k == 0) { // primitive so cannot be null
+ sb.append("Missing k parameter: ").append(new Gson().toJson(info)).append("\n");
+ } else if (info.family == null) {
+ sb.append("Missing sketch family: ").append(new Gson().toJson(info)).append("\n");
+ } else if (isDistinctCounting(familyFromString(info.family)) && info.type == null) {
+ sb.append("Missing value type: ").append(new Gson().toJson(info)).append("\n");
+ }
+ }
+
+ return sb.length() == 0 ? null : sb.toString();
+ }
+
private void parseConfig(final JsonElement config) throws IOException {
final Gson gson = new Gson();
@@ -104,10 +131,21 @@ class SketchServerConfig {
} else if (name.toLowerCase().startsWith(CONFIG_SET_PREFIX)) {
// set* has a common name and type with an array of name names
final JsonObject sketchSetInfo = confEntry.get(name).getAsJsonObject();
+ if (!sketchSetInfo.has(CONFIG_K_FIELD)) {
+ throw new IOException("Missing field: \"" + CONFIG_K_FIELD + " \": " + sketchSetInfo);
+ }
final int k = sketchSetInfo.get(CONFIG_K_FIELD).getAsInt();
+
+ if (!sketchSetInfo.has(CONFIG_FAMILY_FIELD)) {
+ throw new IOException("Missing field: \"" + CONFIG_FAMILY_FIELD + " \": " + sketchSetInfo);
+ }
final String family = sketchSetInfo.get(CONFIG_FAMILY_FIELD).getAsString();
+
String type = null;
- if (isDistinctCounting(BaseSketchesQueryHandler.familyFromString(family))) {
+ if (isDistinctCounting(familyFromString(family))) {
+ if (!sketchSetInfo.has(CONFIG_TYPE_FIELD)) {
+ throw new IOException("Missing field: \"" + CONFIG_TYPE_FIELD + " \": " + sketchSetInfo);
+ }
type = sketchSetInfo.get(CONFIG_TYPE_FIELD).getAsString();
}
final String[] nameList = gson.fromJson(sketchSetInfo.get(CONFIG_SET_NAMES_FIELD).getAsJsonArray(), String[].class);
@@ -119,5 +157,10 @@ class SketchServerConfig {
} else {
throw new IOException("Expected JsonArray or JsonObject but none found");
}
+
+ final String validation = validateSketchInfo();
+ if (validation != null) {
+ throw new IllegalArgumentException(validation);
+ }
}
}
diff --git a/src/main/java/org/apache/datasketches/server/SketchStorage.java b/src/main/java/org/apache/datasketches/server/SketchStorage.java
index e8bc491..6811013 100644
--- a/src/main/java/org/apache/datasketches/server/SketchStorage.java
+++ b/src/main/java/org/apache/datasketches/server/SketchStorage.java
@@ -153,13 +153,16 @@ public class SketchStorage {
sketchMap = new HashMap<>(list.size());
for (final SketchServerConfig.SketchInfo info : list) {
+ if (info.name == null) {
+ throw new IllegalArgumentException("Mussing sketch name:" + info);
+ }
if (sketchMap.containsKey(info.name)) {
throw new IllegalArgumentException("Duplicate sketch key: " + info.name);
}
SketchEntry sketchEntry = null;
final Family family = BaseSketchesQueryHandler.familyFromString(info.family);
- final int k = info.k; // to reduce derferences in code later
+ final int k = info.k; // to reduce dereferences in code later
switch (family) {
case QUICKSELECT:
diff --git a/src/main/java/org/apache/datasketches/server/UpdateHandler.java b/src/main/java/org/apache/datasketches/server/UpdateHandler.java
index 84dc15a..b6ff1ff 100644
--- a/src/main/java/org/apache/datasketches/server/UpdateHandler.java
+++ b/src/main/java/org/apache/datasketches/server/UpdateHandler.java
@@ -32,6 +32,8 @@ import org.apache.datasketches.theta.Union;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
+import sun.security.acl.AclEntryImpl;
+
import static org.apache.datasketches.server.SketchConstants.*;
@@ -76,8 +78,8 @@ public class UpdateHandler extends BaseSketchesQueryHandler {
final SketchStorage.SketchEntry se = sketches.getSketch(name);
final JsonElement data = entry.getValue();
- if (name == null || se == null || data == null) {
- throw new IllegalArgumentException("Attempt to call update with missing name or sketch not found");
+ if (se == null || data == null) {
+ throw new IllegalArgumentException("Sketch not found or update with no data value(s)");
}
synchronized (name.intern()) {
@@ -98,7 +100,6 @@ public class UpdateHandler extends BaseSketchesQueryHandler {
final JsonArray data) {
switch (entry.family_) {
case UNION: // theta
- assert(entry.type_ != null);
switch (entry.type_) {
case FLOAT: case DOUBLE:
for (final JsonElement e : data) { ((Union) entry.sketch_).update(e.getAsDouble()); }
@@ -113,7 +114,6 @@ public class UpdateHandler extends BaseSketchesQueryHandler {
break;
case CPC:
- assert(entry.type_ != null);
switch (entry.type_) {
case FLOAT: case DOUBLE:
for (final JsonElement e : data) { ((CpcSketch) entry.sketch_).update(e.getAsDouble()); }
@@ -128,16 +128,15 @@ public class UpdateHandler extends BaseSketchesQueryHandler {
break;
case HLL:
- assert(entry.type_ != null);
switch (entry.type_) {
case FLOAT: case DOUBLE:
for (final JsonElement e : data) { ((HllSketch) entry.sketch_).update(e.getAsDouble()); }
break;
case INT: case LONG:
- for (final JsonElement e : data) { ((CpcSketch) entry.sketch_).update(e.getAsLong()); }
+ for (final JsonElement e : data) { ((HllSketch) entry.sketch_).update(e.getAsLong()); }
break;
case STRING: default:
- for (final JsonElement e : data) { ((CpcSketch) entry.sketch_).update(e.getAsString()); }
+ for (final JsonElement e : data) { ((HllSketch) entry.sketch_).update(e.getAsString()); }
break;
}
break;
@@ -148,18 +147,7 @@ public class UpdateHandler extends BaseSketchesQueryHandler {
case FREQUENCY:
for (final JsonElement e : data) {
- if (e.isJsonObject()) {
- final JsonObject inputPair = e.getAsJsonObject();
- if (!inputPair.has(QUERY_PAIR_ITEM_FIELD) || !inputPair.has(QUERY_PAIR_WEIGHT_FIELD)) {
- throw new IllegalArgumentException("Frequent Items input pairs must include both "
- + QUERY_PAIR_ITEM_FIELD + " and " + QUERY_PAIR_WEIGHT_FIELD + " values");
- }
- final String item = inputPair.get(QUERY_PAIR_ITEM_FIELD).getAsString();
- final int weight = inputPair.get(QUERY_PAIR_WEIGHT_FIELD).getAsInt();
- ((ItemsSketch<String>) entry.sketch_).update(item, weight);
- } else {
- ((ItemsSketch<String>) entry.sketch_).update(e.getAsString());
- }
+ applyFrequentItemsUpdate((ItemsSketch<String>) entry.sketch_, e);
}
break;
@@ -169,18 +157,7 @@ public class UpdateHandler extends BaseSketchesQueryHandler {
case VAROPT:
for (final JsonElement e : data) {
- if (e.isJsonObject()) {
- final JsonObject inputPair = e.getAsJsonObject();
- if (!inputPair.has(QUERY_PAIR_ITEM_FIELD) || !inputPair.has(QUERY_PAIR_WEIGHT_FIELD)) {
- throw new IllegalArgumentException("VarOpt input pairs must include both "
- + QUERY_PAIR_ITEM_FIELD + " and " + QUERY_PAIR_WEIGHT_FIELD + " values");
- }
- final String item = inputPair.get(QUERY_PAIR_ITEM_FIELD).getAsString();
- final double weight = inputPair.get(QUERY_PAIR_WEIGHT_FIELD).getAsDouble();
- ((VarOptItemsSketch<String>) entry.sketch_).update(item, weight);
- } else {
- ((VarOptItemsSketch<String>) entry.sketch_).update(e.getAsString(), 1.0);
- }
+ applyVarOptUpdate((VarOptItemsSketch<String>) entry.sketch_, e);
}
break;
@@ -194,7 +171,6 @@ public class UpdateHandler extends BaseSketchesQueryHandler {
final JsonElement data) {
switch (entry.family_) {
case UNION:
- assert(entry.type_ != null);
switch (entry.type_) {
case FLOAT: case DOUBLE:
((Union) entry.sketch_).update(data.getAsDouble());
@@ -209,7 +185,6 @@ public class UpdateHandler extends BaseSketchesQueryHandler {
break;
case CPC:
- assert(entry.type_ != null);
switch (entry.type_) {
case FLOAT: case DOUBLE:
((CpcSketch) entry.sketch_).update(data.getAsDouble());
@@ -224,7 +199,6 @@ public class UpdateHandler extends BaseSketchesQueryHandler {
break;
case HLL:
- assert(entry.type_ != null);
switch (entry.type_) {
case FLOAT: case DOUBLE:
((HllSketch) entry.sketch_).update(data.getAsDouble());
@@ -243,18 +217,7 @@ public class UpdateHandler extends BaseSketchesQueryHandler {
break;
case FREQUENCY:
- if (data.isJsonObject()) {
- final JsonObject inputPair = data.getAsJsonObject();
- if (!inputPair.has(QUERY_PAIR_ITEM_FIELD) || !inputPair.has(QUERY_PAIR_WEIGHT_FIELD)) {
- throw new IllegalArgumentException("Frequent Items input pairs must include both "
- + QUERY_PAIR_ITEM_FIELD + " and " + QUERY_PAIR_WEIGHT_FIELD + " values");
- }
- final String item = inputPair.get(QUERY_PAIR_ITEM_FIELD).getAsString();
- final int weight = inputPair.get(QUERY_PAIR_WEIGHT_FIELD).getAsInt();
- ((ItemsSketch<String>) entry.sketch_).update(item, weight);
- } else {
- ((ItemsSketch<String>) entry.sketch_).update(data.getAsString());
- }
+ applyFrequentItemsUpdate((ItemsSketch<String>) entry.sketch_, data);
break;
case RESERVOIR:
@@ -262,18 +225,7 @@ public class UpdateHandler extends BaseSketchesQueryHandler {
break;
case VAROPT:
- if (data.isJsonObject()) {
- final JsonObject inputPair = data.getAsJsonObject();
- if (!inputPair.has(QUERY_PAIR_ITEM_FIELD) || !inputPair.has(QUERY_PAIR_WEIGHT_FIELD)) {
- throw new IllegalArgumentException("VarOpt input pairs must include both "
- + QUERY_PAIR_ITEM_FIELD + " and " + QUERY_PAIR_WEIGHT_FIELD + " values");
- }
- final String item = inputPair.get(QUERY_PAIR_ITEM_FIELD).getAsString();
- final double weight = inputPair.get(QUERY_PAIR_WEIGHT_FIELD).getAsDouble();
- ((VarOptItemsSketch<String>) entry.sketch_).update(item, weight);
- } else {
- ((VarOptItemsSketch<String>) entry.sketch_).update(data.getAsString(), 1.0);
- }
+ applyVarOptUpdate((VarOptItemsSketch<String>) entry.sketch_, data);
break;
default:
@@ -281,5 +233,45 @@ public class UpdateHandler extends BaseSketchesQueryHandler {
}
}
+ /**
+ * Handles simple item and (item, weight) pairs for FrequentItems updates
+ * @param fiSketch The frequent items sketch to receive the update
+ * @param data The JSON data, whether an raw value or object
+ */
+ private static void applyFrequentItemsUpdate(final ItemsSketch<String> fiSketch, final JsonElement data) {
+ if (data.isJsonObject()) {
+ final JsonObject inputPair = data.getAsJsonObject();
+ validateItemWeightPair(inputPair); // throws on error;
+ final String item = inputPair.get(QUERY_PAIR_ITEM_FIELD).getAsString();
+ final int weight = inputPair.get(QUERY_PAIR_WEIGHT_FIELD).getAsInt();
+ fiSketch.update(item, weight);
+ } else {
+ fiSketch.update(data.getAsString());
+ }
+ }
+
+ /**
+ * Handles simple item and (item, weight) pairs for VarOpt updates
+ * @param voSketch The varopt sketch to receive the update
+ * @param data The JSON data, whether an raw value or object
+ */
+ private static void applyVarOptUpdate(final VarOptItemsSketch<String> voSketch, final JsonElement data) {
+ if (data.isJsonObject()) {
+ final JsonObject inputPair = data.getAsJsonObject();
+ validateItemWeightPair(inputPair); // throws on error
+ final String item = inputPair.get(QUERY_PAIR_ITEM_FIELD).getAsString();
+ final int weight = inputPair.get(QUERY_PAIR_WEIGHT_FIELD).getAsInt();
+ voSketch.update(item, weight);
+ } else {
+ voSketch.update(data.getAsString(), 1.0);
+ }
+ }
+
+ private static void validateItemWeightPair(final JsonObject inputPair) {
+ if (!inputPair.has(QUERY_PAIR_ITEM_FIELD) || !inputPair.has(QUERY_PAIR_WEIGHT_FIELD)) {
+ throw new IllegalArgumentException("Frequent Items input pairs must include both "
+ + QUERY_PAIR_ITEM_FIELD + " and " + QUERY_PAIR_WEIGHT_FIELD + " values");
+ }
+ }
}
diff --git a/src/test/java/org/apache/datasketches/server/ServerTestBase.java b/src/test/java/org/apache/datasketches/server/ServerTestBase.java
index 8c0d7f5..59cee6c 100644
--- a/src/test/java/org/apache/datasketches/server/ServerTestBase.java
+++ b/src/test/java/org/apache/datasketches/server/ServerTestBase.java
@@ -19,10 +19,11 @@
package org.apache.datasketches.server;
+import static org.apache.datasketches.server.SketchConstants.ERROR_KEY;
import static org.testng.Assert.fail;
-import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
+import java.util.Map;
import java.util.Objects;
import java.io.DataOutputStream;
import java.io.InputStreamReader;
@@ -31,9 +32,12 @@ import java.net.URL;
import java.nio.charset.StandardCharsets;
import org.checkerframework.checker.nullness.qual.NonNull;
+import org.eclipse.jetty.http.HttpStatus;
import org.testng.annotations.AfterClass;
import org.testng.annotations.BeforeClass;
+
+import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
@@ -51,6 +55,7 @@ public class ServerTestBase {
server_.start();
serverUri_ = server_.getURI();
} catch (final Exception e) {
+ System.err.println(e.getMessage());
fail();
}
}
@@ -66,11 +71,18 @@ public class ServerTestBase {
}
}
+ static void clearResponse(@NonNull final JsonObject response) {
+ for (final Map.Entry<String, JsonElement> entry : response.entrySet()) {
+ response.remove(entry.getKey());
+ }
+ }
+
int postData(@NonNull final String path,
@NonNull final JsonObject data,
@NonNull final JsonObject response) {
HttpURLConnection http = null;
int status = -1;
+ clearResponse(response);
try {
// set up the POST
@@ -91,14 +103,19 @@ public class ServerTestBase {
}
status = http.getResponseCode();
- if (status == HttpServletResponse.SC_OK) {
+ if (status == HttpStatus.OK_200) {
// read response, if any, and put into a JSON element
try (final InputStreamReader isr = new InputStreamReader(http.getInputStream())) {
response.add(RESPONSE_FIELD, JsonParser.parseReader(isr));
}
+ } else if (status == HttpStatus.UNPROCESSABLE_ENTITY_422) {
+ // read error response and put into a JSON element
+ try (final InputStreamReader isr = new InputStreamReader(http.getErrorStream())) {
+ response.add(ERROR_KEY, JsonParser.parseReader(isr));
+ }
}
} catch (final IOException e) {
- fail();
+ fail();
} finally {
if (http != null)
http.disconnect();
@@ -112,9 +129,10 @@ public class ServerTestBase {
@NonNull final JsonObject response) {
HttpURLConnection http = null;
int status = -1;
+ clearResponse(response);
try {
- // set up the POST
+ // set up the GET
final URL url = new URL(serverUri_ + path + "?" + data);
http = (HttpURLConnection) url.openConnection();
http.setDoInput(true);
@@ -122,11 +140,16 @@ public class ServerTestBase {
http.connect();
status = http.getResponseCode();
- if (status == HttpServletResponse.SC_OK) {
+ if (status == HttpStatus.OK_200) {
// read response, if any, and put into a JSON element
try (final InputStreamReader isr = new InputStreamReader(http.getInputStream())) {
response.add(RESPONSE_FIELD, JsonParser.parseReader(isr));
}
+ } else if (status == HttpStatus.UNPROCESSABLE_ENTITY_422) {
+ // read error response and put into a JSON element
+ try (final InputStreamReader isr = new InputStreamReader(http.getErrorStream())) {
+ response.add(ERROR_KEY, JsonParser.parseReader(isr));
+ }
}
} catch (final IOException e) {
fail();
diff --git a/src/test/java/org/apache/datasketches/server/SketchStorageTest.java b/src/test/java/org/apache/datasketches/server/SketchStorageTest.java
index 03f4b5c..11d5e40 100644
--- a/src/test/java/org/apache/datasketches/server/SketchStorageTest.java
+++ b/src/test/java/org/apache/datasketches/server/SketchStorageTest.java
@@ -69,7 +69,7 @@ public class SketchStorageTest {
final SketchStorage storage = new SketchStorage(serverConfig.getSketchList());
final JsonObject sketches = storage.listSketches();
assertTrue(sketches.has(RESPONSE_SKETCH_COUNT_FIELD));
- assertEquals(sketches.get(RESPONSE_SKETCH_COUNT_FIELD).getAsInt(), 15);
- assertTrue(storage.contains("cpcOfNumbers"));
+ assertEquals(sketches.get(RESPONSE_SKETCH_COUNT_FIELD).getAsInt(), 20);
+ assertTrue(storage.contains("cpcOfFloats"));
}
}
diff --git a/src/test/java/org/apache/datasketches/server/UpdateHandlerTest.java b/src/test/java/org/apache/datasketches/server/UpdateHandlerTest.java
index a77369b..024bb5d 100644
--- a/src/test/java/org/apache/datasketches/server/UpdateHandlerTest.java
+++ b/src/test/java/org/apache/datasketches/server/UpdateHandlerTest.java
@@ -25,53 +25,131 @@ import static org.apache.datasketches.server.SketchConstants.UPDATE_PATH;
import static org.testng.Assert.assertEquals;
import static org.testng.Assert.assertTrue;
-import javax.servlet.http.HttpServletResponse;
-
import org.apache.datasketches.cpc.CpcSketch;
+import org.apache.datasketches.hll.HllSketch;
+import org.apache.datasketches.kll.KllFloatsSketch;
+import org.apache.datasketches.sampling.ReservoirItemsSketch;
+import org.apache.datasketches.sampling.VarOptItemsSketch;
+import org.apache.datasketches.theta.CompactSketch;
+import org.apache.datasketches.theta.Union;
+import org.eclipse.jetty.http.HttpStatus;
import org.testng.annotations.Test;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
-
+import com.google.gson.JsonPrimitive;
public class UpdateHandlerTest extends ServerTestBase {
/* The tests here are going to be structured very similarly. It might be possible
- * to find a common framework and reduce the amount o repetition? But not clear with
- * type erasure as opposed to C++-style templates.
+ * to find a common framework and reduce the amount ofXS repetition?
*/
+ @Test
+ public void emptyUpdate() {
+ final JsonObject response = new JsonObject();
+ JsonObject request = new JsonObject();
+
+ // completely empty is ok
+ assertEquals(postData(UPDATE_PATH, request, response), HttpStatus.OK_200);
+
+ // invalid name with value
+ request.addProperty("sketchDoesNotExist", "validStringValue");
+ assertEquals(postData(UPDATE_PATH, request, response), HttpStatus.UNPROCESSABLE_ENTITY_422);
+
+ // valid name, empty value
+ request = new JsonObject();
+ request.add("hll1", new JsonObject());
+ assertEquals(postData(UPDATE_PATH, request, response), HttpStatus.UNPROCESSABLE_ENTITY_422);
+ }
@Test
- public void cpcUpdate() {
+ public void cpcUpdate1() {
final JsonObject response = new JsonObject();
- final String sketchName = "cpcOfNumbers";
+ final String sketchName = "cpcOfLongs";
final int nPoints = 1000;
// testing using both GET and POST
- int status;
-
JsonObject request = new JsonObject();
JsonArray data = new JsonArray();
for (int i = 0; i < nPoints; ++i)
data.add(i);
request.add(sketchName, data);
- status = postData(UPDATE_PATH, request, response);
- assertEquals(status, HttpServletResponse.SC_OK);
+ assertEquals(postData(UPDATE_PATH, request, response), HttpStatus.OK_200);
request = new JsonObject();
data = new JsonArray();
for (int i = nPoints; i < 2 * nPoints; ++i)
data.add(i);
request.add(sketchName, data);
- status = getData(UPDATE_PATH, request, response);
- assertEquals(status, HttpServletResponse.SC_OK);
+ assertEquals(getData(UPDATE_PATH, request, response), HttpStatus.OK_200);
+
+ // single-item update
+ request = new JsonObject();
+ request.add(sketchName, new JsonPrimitive(-1));
+ assertEquals(getData(UPDATE_PATH, request, response), HttpStatus.OK_200);
final JsonElement element = response.get(RESPONSE_FIELD);
assertTrue(element.isJsonNull());
final SketchStorage.SketchEntry entry = server_.getSketch(sketchName);
final CpcSketch sk = (CpcSketch) entry.sketch_;
- assertEquals(sk.getEstimate(), 2 * nPoints, 2 * nPoints * 1e-2);
+ assertEquals(entry.type_, ValueType.LONG);
+ assertTrue(2 * nPoints + 1 <= sk.getUpperBound(1));
+ assertTrue(2 * nPoints + 1 >= sk.getLowerBound(1));
+ }
+
+ @Test
+ public void cpcUpdate2() {
+ final JsonObject response = new JsonObject();
+ final String sketchName = "cpcOfStrings";
+ final int nPoints = 5000;
+
+ JsonObject request = new JsonObject();
+ final JsonArray data = new JsonArray();
+ for (int i = 0; i < nPoints; ++i)
+ data.add(i); // not converting to String since should happen upon parsing
+ request.add(sketchName, data);
+ assertEquals(postData(UPDATE_PATH, request, response), HttpStatus.OK_200);
+
+ request = new JsonObject();
+ request.add(sketchName, new JsonPrimitive(-1));
+ assertEquals(postData(UPDATE_PATH, request, response), HttpStatus.OK_200);
+
+ final JsonElement element = response.get(RESPONSE_FIELD);
+ assertTrue(element.isJsonNull());
+
+ final SketchStorage.SketchEntry entry = server_.getSketch(sketchName);
+ final CpcSketch sk = (CpcSketch) entry.sketch_;
+ assertEquals(entry.type_, ValueType.STRING);
+ assertTrue(nPoints + 1 <= sk.getUpperBound(1));
+ assertTrue(nPoints + 1 >= sk.getLowerBound(1));
+ }
+
+ @Test
+ public void cpcUpdate3() {
+ final JsonObject response = new JsonObject();
+ final String sketchName = "cpcOfFloats";
+ final int nPoints = 2500;
+
+ JsonObject request = new JsonObject();
+ final JsonArray data = new JsonArray();
+ for (int i = 0; i < nPoints; ++i)
+ data.add(i * 10.0);
+ request.add(sketchName, data);
+ assertEquals(postData(UPDATE_PATH, request, response), HttpStatus.OK_200);
+
+ request = new JsonObject();
+ request.add(sketchName, new JsonPrimitive(-1));
+ assertEquals(getData(UPDATE_PATH, request, response), HttpStatus.OK_200);
+
+ final JsonElement element = response.get(RESPONSE_FIELD);
+ assertTrue(element.isJsonNull());
+
+ final SketchStorage.SketchEntry entry = server_.getSketch(sketchName);
+ final CpcSketch sk = (CpcSketch) entry.sketch_;
+ assertEquals(entry.type_, ValueType.FLOAT);
+ assertTrue(nPoints + 1 <= sk.getUpperBound(1));
+ assertTrue(nPoints + 1 >= sk.getLowerBound(1));
}
@Test
@@ -83,7 +161,7 @@ public class UpdateHandlerTest extends ServerTestBase {
// single item
JsonObject request = new JsonObject();
request.addProperty(sketchName, "item1");
- assertEquals(postData(UPDATE_PATH, request, response), HttpServletResponse.SC_OK);
+ assertEquals(postData(UPDATE_PATH, request, response), HttpStatus.OK_200);
// item with weight
request = new JsonObject();
@@ -91,7 +169,7 @@ public class UpdateHandlerTest extends ServerTestBase {
data.addProperty(QUERY_PAIR_ITEM_FIELD, "item2");
data.addProperty(QUERY_PAIR_WEIGHT_FIELD, 5);
request.add(sketchName, data);
- assertEquals(postData(UPDATE_PATH, request, response), HttpServletResponse.SC_OK);
+ assertEquals(postData(UPDATE_PATH, request, response), HttpStatus.OK_200);
// array of items with and without weights
request = new JsonObject();
@@ -102,7 +180,7 @@ public class UpdateHandlerTest extends ServerTestBase {
data.addProperty(QUERY_PAIR_WEIGHT_FIELD, 10);
dataArray.add(data);
request.add(sketchName, dataArray);
- assertEquals(postData(UPDATE_PATH, request, response), HttpServletResponse.SC_OK);
+ assertEquals(postData(UPDATE_PATH, request, response), HttpStatus.OK_200);
final JsonElement element = response.get(RESPONSE_FIELD);
assertTrue(element.isJsonNull());
@@ -112,31 +190,294 @@ public class UpdateHandlerTest extends ServerTestBase {
assertEquals(sk.getEstimate("item1"), 2);
assertEquals(sk.getEstimate("item2"), 5);
assertEquals(sk.getEstimate("item3"), 10);
+
+ // update with malformed item/weight pair (each independently)
+ request = new JsonObject();
+ data = new JsonObject();
+ data.addProperty("invalid", "item2");
+ data.addProperty(QUERY_PAIR_WEIGHT_FIELD, 5);
+ request.add(sketchName, data);
+ assertEquals(postData(UPDATE_PATH, request, response), HttpStatus.UNPROCESSABLE_ENTITY_422);
+
+ data = new JsonObject();
+ data.addProperty(QUERY_PAIR_ITEM_FIELD, "item2");
+ data.addProperty("invalid", 5);
+ request.add(sketchName, data);
+ assertEquals(postData(UPDATE_PATH, request, response), HttpStatus.UNPROCESSABLE_ENTITY_422);
}
@Test
public void hllUpdate() {
- // update multiple sketches from an array
+ final JsonObject response = new JsonObject();
+ final String sketchName = "hll4";
+ final int nPoints = 100;
+
+ JsonObject request = new JsonObject();
+ final JsonArray data = new JsonArray();
+ for (int i = 0; i < nPoints; ++i)
+ data.add(i); // not converting to String since should happen upon parsing
+ request.add(sketchName, data);
+ assertEquals(postData(UPDATE_PATH, request, response), HttpStatus.OK_200);
+
+ request = new JsonObject();
+ request.add(sketchName, new JsonPrimitive(-1));
+ assertEquals(postData(UPDATE_PATH, request, response), HttpStatus.OK_200);
+
+ final JsonElement element = response.get(RESPONSE_FIELD);
+ assertTrue(element.isJsonNull());
+
+ final SketchStorage.SketchEntry entry = server_.getSketch(sketchName);
+ final HllSketch sk = (HllSketch) entry.sketch_;
+ assertEquals(entry.type_, ValueType.STRING);
+ assertTrue(nPoints + 1 <= sk.getUpperBound(1));
+ assertTrue(nPoints + 1 >= sk.getLowerBound(1));
+ }
+
+ @Test
+ public void hllUpdate2() {
+ final JsonObject response = new JsonObject();
+ final String sketchName = "hllOfInts";
+ final int nPoints = 20; // should be in exact mode
+
+ JsonObject request = new JsonObject();
+ JsonArray data = new JsonArray();
+ for (int i = 0; i < nPoints; ++i)
+ data.add(i);
+ request.add(sketchName, data);
+ assertEquals(getData(UPDATE_PATH, request, response), HttpStatus.OK_200);
+
+ request = new JsonObject();
+ data = new JsonArray();
+ for (int i = nPoints; i < 2 * nPoints; ++i)
+ data.add(i);
+ request.add(sketchName, data);
+ assertEquals(getData(UPDATE_PATH, request, response), HttpStatus.OK_200);
+
+ // single-item update
+ request = new JsonObject();
+ request.add(sketchName, new JsonPrimitive(-1));
+ assertEquals(getData(UPDATE_PATH, request, response), HttpStatus.OK_200);
+
+ final JsonElement element = response.get(RESPONSE_FIELD);
+ assertTrue(element.isJsonNull());
+
+ final SketchStorage.SketchEntry entry = server_.getSketch(sketchName);
+ final HllSketch sk = (HllSketch) entry.sketch_;
+ assertEquals(entry.type_, ValueType.INT);
+ assertTrue(2 * nPoints + 1 <= sk.getUpperBound(1));
+ assertTrue(2 * nPoints + 1 >= sk.getLowerBound(1));
+ }
+
+ @Test
+ public void hllUpdate3() {
+ final JsonObject response = new JsonObject();
+ final String sketchName = "hllOfDoubles";
+ final int nPoints = 479;
+
+ JsonObject request = new JsonObject();
+ final JsonArray data = new JsonArray();
+ for (int i = 0; i < nPoints; ++i)
+ data.add(i * 10.0);
+ request.add(sketchName, data);
+ assertEquals(postData(UPDATE_PATH, request, response), HttpStatus.OK_200);
+
+ request = new JsonObject();
+ request.add(sketchName, new JsonPrimitive(-1));
+ assertEquals(getData(UPDATE_PATH, request, response), HttpStatus.OK_200);
+
+ final JsonElement element = response.get(RESPONSE_FIELD);
+ assertTrue(element.isJsonNull());
+
+ final SketchStorage.SketchEntry entry = server_.getSketch(sketchName);
+ final HllSketch sk = (HllSketch) entry.sketch_;
+ assertEquals(entry.type_, ValueType.DOUBLE);
+ assertTrue(nPoints + 1 <= sk.getUpperBound(1));
+ assertTrue(nPoints + 1 >= sk.getLowerBound(1));
}
@Test
public void kllUpdate() {
+ final JsonObject response = new JsonObject();
+ final String sketchName = "duration";
+ final int nPoints = 953;
+ final int nUpdates = 5;
+
+ for (int j = 0; j < nUpdates; ++j) {
+ // batch update
+ JsonObject request = new JsonObject();
+ final JsonArray data = new JsonArray();
+ for (int i = 0; i < nPoints; ++i)
+ data.add(i * 10.0);
+ request.add(sketchName, data);
+ assertEquals(postData(UPDATE_PATH, request, response), HttpStatus.OK_200);
+ assertTrue(response.get(RESPONSE_FIELD).isJsonNull());
+ // single update
+ request = new JsonObject();
+ request.add(sketchName, new JsonPrimitive(-1));
+ assertEquals(getData(UPDATE_PATH, request, response), HttpStatus.OK_200);
+ assertTrue(response.get(RESPONSE_FIELD).isJsonNull());
+ }
+
+ final SketchStorage.SketchEntry entry = server_.getSketch(sketchName);
+ final KllFloatsSketch sk = (KllFloatsSketch) entry.sketch_;
+ assertEquals(sk.getN(), (nPoints + 1) * nUpdates);
}
@Test
- public void thetaUpdate() {
+ public void thetaUpdate1() {
+ final JsonObject response = new JsonObject();
+ final String sketchName = "theta0";
+ final int nPoints = 1000;
+
+ JsonObject request = new JsonObject();
+ JsonArray data = new JsonArray();
+ for (int i = 0; i < nPoints; ++i)
+ data.add(i);
+ request.add(sketchName, data);
+ assertEquals(postData(UPDATE_PATH, request, response), HttpStatus.OK_200);
+ request = new JsonObject();
+ data = new JsonArray();
+ for (int i = nPoints; i < 2 * nPoints; ++i)
+ data.add(i);
+ request.add(sketchName, data);
+ assertEquals(getData(UPDATE_PATH, request, response), HttpStatus.OK_200);
+
+ // single-item update
+ request = new JsonObject();
+ request.add(sketchName, new JsonPrimitive(-1));
+ assertEquals(getData(UPDATE_PATH, request, response), HttpStatus.OK_200);
+
+ final JsonElement element = response.get(RESPONSE_FIELD);
+ assertTrue(element.isJsonNull());
+
+ final SketchStorage.SketchEntry entry = server_.getSketch(sketchName);
+ final CompactSketch sk = ((Union) entry.sketch_).getResult();
+ assertEquals(entry.type_, ValueType.INT);
+ assertTrue(2 * nPoints + 1 <= sk.getUpperBound(1));
+ assertTrue(2 * nPoints + 1 >= sk.getLowerBound(1));
}
@Test
- public void reservoirUpdate() {
+ public void thetaUpdate2() {
+ final JsonObject response = new JsonObject();
+ final String sketchName = "thetaOfStrings";
+ final int nPoints = 5000;
+ JsonObject request = new JsonObject();
+ final JsonArray data = new JsonArray();
+ for (int i = 0; i < nPoints; ++i)
+ data.add(i); // not converting to String since should happen upon parsing
+ request.add(sketchName, data);
+ assertEquals(postData(UPDATE_PATH, request, response), HttpStatus.OK_200);
+
+ request = new JsonObject();
+ request.add(sketchName, new JsonPrimitive(-1));
+ assertEquals(postData(UPDATE_PATH, request, response), HttpStatus.OK_200);
+
+ final JsonElement element = response.get(RESPONSE_FIELD);
+ assertTrue(element.isJsonNull());
+
+ final SketchStorage.SketchEntry entry = server_.getSketch(sketchName);
+ final CompactSketch sk = ((Union) entry.sketch_).getResult();
+ assertEquals(entry.type_, ValueType.STRING);
+ assertTrue(nPoints + 1 <= sk.getUpperBound(1));
+ assertTrue(nPoints + 1 >= sk.getLowerBound(1));
}
@Test
- public void voUpdate() {
+ public void thetaUpdate3() {
+ final JsonObject response = new JsonObject();
+ final String sketchName = "thetaOfDoubles";
+ final int nPoints = 1153;
+ JsonObject request = new JsonObject();
+ final JsonArray data = new JsonArray();
+ for (int i = 0; i < nPoints; ++i)
+ data.add(i * 10.0);
+ request.add(sketchName, data);
+ assertEquals(postData(UPDATE_PATH, request, response), HttpStatus.OK_200);
+
+ request = new JsonObject();
+ request.add(sketchName, new JsonPrimitive(-1));
+ assertEquals(getData(UPDATE_PATH, request, response), HttpStatus.OK_200);
+
+ final JsonElement element = response.get(RESPONSE_FIELD);
+ assertTrue(element.isJsonNull());
+
+ final SketchStorage.SketchEntry entry = server_.getSketch(sketchName);
+ final CompactSketch sk = ((Union) entry.sketch_).getResult();
+ assertEquals(entry.type_, ValueType.DOUBLE);
+ assertTrue(nPoints + 1 <= sk.getUpperBound(1));
+ assertTrue(nPoints + 1 >= sk.getLowerBound(1));
}
+ @Test
+ @SuppressWarnings("unchecked")
+ public void reservoirUpdate() {
+ final JsonObject response = new JsonObject();
+ final String sketchName = "rs";
+ final int nPoints = 20;
+ final int nUpdates = 5;
+
+ // just going to use numbers as Strings to avoid creating a dictionary
+ for (int j = 0; j < nUpdates; ++j) {
+ JsonObject request = new JsonObject();
+ final JsonArray data = new JsonArray();
+ for (int i = 0; i < nPoints; ++i)
+ data.add(Integer.toString(i));
+ request.add(sketchName, data);
+ assertEquals(postData(UPDATE_PATH, request, response), HttpStatus.OK_200);
+ assertTrue(response.get(RESPONSE_FIELD).isJsonNull());
+
+ // single update
+ request = new JsonObject();
+ request.add(sketchName, new JsonPrimitive("-1"));
+ assertEquals(getData(UPDATE_PATH, request, response), HttpStatus.OK_200);
+ assertTrue(response.get(RESPONSE_FIELD).isJsonNull());
+ }
+
+ final SketchStorage.SketchEntry entry = server_.getSketch(sketchName);
+ final ReservoirItemsSketch<String> sk = (ReservoirItemsSketch<String>) entry.sketch_;
+ assertEquals(sk.getN(), (nPoints + 1) * nUpdates);
+ }
+
+ @Test
+ @SuppressWarnings("unchecked")
+ public void varOptUpdate() {
+ final JsonObject response = new JsonObject();
+ final String sketchName = "vo";
+
+ // single item
+ JsonObject request = new JsonObject();
+ request.addProperty(sketchName, "item1");
+ assertEquals(postData(UPDATE_PATH, request, response), HttpStatus.OK_200);
+ assertTrue(response.get(RESPONSE_FIELD).isJsonNull());
+
+ // item with weight
+ request = new JsonObject();
+ JsonObject data = new JsonObject();
+ data.addProperty(QUERY_PAIR_ITEM_FIELD, "item2");
+ data.addProperty(QUERY_PAIR_WEIGHT_FIELD, 5);
+ request.add(sketchName, data);
+ assertEquals(postData(UPDATE_PATH, request, response), HttpStatus.OK_200);
+ assertTrue(response.get(RESPONSE_FIELD).isJsonNull());
+
+ // array of items with and without weights
+ request = new JsonObject();
+ final JsonArray dataArray = new JsonArray();
+ dataArray.add("item1"); // that it's a duplicate doens't matter
+ data = new JsonObject();
+ data.addProperty(QUERY_PAIR_ITEM_FIELD, "item3");
+ data.addProperty(QUERY_PAIR_WEIGHT_FIELD, 10);
+ dataArray.add(data);
+ request.add(sketchName, dataArray);
+ assertEquals(postData(UPDATE_PATH, request, response), HttpStatus.OK_200);
+ assertTrue(response.get(RESPONSE_FIELD).isJsonNull());
+
+ final SketchStorage.SketchEntry entry = server_.getSketch(sketchName);
+ final VarOptItemsSketch<String> sk = (VarOptItemsSketch<String>) entry.sketch_;
+ assertEquals(sk.getN(), 4);
+ }
}
diff --git a/src/test/resources/test_config.json b/src/test/resources/test_config.json
index a685c17..2acd6bd 100644
--- a/src/test/resources/test_config.json
+++ b/src/test/resources/test_config.json
@@ -1,56 +1,92 @@
{
"port": 0,
"sketches_A": [
- { "name": "cpcOfNumbers",
- "k": 12,
- "type": "long",
- "family": "cpc"
+ {
+ "name": "cpcOfLongs",
+ "k": 12,
+ "type": "long",
+ "family": "cpc"
+ },
+ {
+ "name": "cpcOfFloats",
+ "k": 12,
+ "type": "float",
+ "family": "cpc"
+ },
+ {
+ "name": "cpcOfStrings",
+ "k": 14,
+ "type": "string",
+ "family": "cpc"
+ },
+ {
+ "name": "thetaOfDoubles",
+ "k": 9,
+ "type": "double",
+ "family": "theta"
+ },
+ {
+ "name": "thetaOfStrings",
+ "k": 16,
+ "type": "string",
+ "family": "theta"
+ }
+ ],
+ "set1": {
+ "family": "hll",
+ "type": "string",
+ "k": 14,
+ "names": [
+ "hll1",
+ "hll2",
+ "hll3",
+ "hll4"
+ ]
},
- { "name": "cpcOfStrings",
- "k": 14,
- "type": "string",
- "family": "cpc"
- }
- ],
- "set1": {
- "family": "hll",
- "type": "string",
- "k": 14,
- "names": [
- "hll1",
- "hll2",
- "hll3",
- "hll4"
- ]
- },
- "set2": {
- "k": 12,
- "family": "theta",
- "type": "int",
- "names": [
- "theta0",
- "theta1",
- "theta2",
- "theta3",
- "theta4"
- ]
- },
- "sketches_B": [
- { "name": "duration",
- "k": "160",
- "family": "kll"
+ "set2": {
+ "k": 12,
+ "family": "theta",
+ "type": "int",
+ "names": [
+ "theta0",
+ "theta1",
+ "theta2",
+ "theta3",
+ "theta4"
+ ]
},
- { "name": "topItems",
- "k": "128",
- "family": "frequency"
- },
- { "name": "rs",
- "k": "20",
- "family": "reservoir"
- },
- { "name": "vo",
- "k": "20",
- "family": "varopt"
- }
+ "sketches_B": [
+ {
+ "name": "duration",
+ "k": "160",
+ "family": "kll"
+ },
+ {
+ "name": "topItems",
+ "k": "128",
+ "family": "frequency"
+ },
+ {
+ "name": "rs",
+ "k": "20",
+ "family": "reservoir"
+ },
+ {
+ "name": "vo",
+ "k": "20",
+ "family": "varopt"
+ },
+ {
+ "name": "hllOfInts",
+ "family": "hll",
+ "k": 10,
+ "type": "int"
+ },
+ {
+ "name": "hllOfDoubles",
+ "family": "hll",
+ "k": 11,
+ "type": "double"
+ }
]
}
---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@datasketches.apache.org
For additional commands, e-mail: commits-help@datasketches.apache.org