Skip to content

Commit 10a1d50

Browse files
committed
feat: DQL for client-side validations failures
1 parent af36753 commit 10a1d50

File tree

5 files changed

+70
-12
lines changed

5 files changed

+70
-12
lines changed

connector/src/main/java/io/questdb/kafka/BufferingSender.java

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,24 @@ public Sender boolColumn(CharSequence name, boolean value) {
9797
return this;
9898
}
9999

100+
@Override
101+
public void cancelRow() {
102+
symbolColumnNames.clear();
103+
symbolColumnValues.clear();
104+
stringNames.clear();
105+
stringValues.clear();
106+
longNames.clear();
107+
longValues.clear();
108+
doubleNames.clear();
109+
doubleValues.clear();
110+
boolNames.clear();
111+
boolValues.clear();
112+
timestampNames.clear();
113+
timestampValues.clear();
114+
115+
sender.cancelRow();
116+
}
117+
100118
@Override
101119
public Sender timestampColumn(CharSequence name, long value, ChronoUnit unit) {
102120
if (symbolColumns.contains(name)) {
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
package io.questdb.kafka;
2+
3+
import io.questdb.std.NumericException;
4+
import org.apache.kafka.connect.errors.ConnectException;
5+
6+
public final class InvalidDataException extends ConnectException {
7+
public InvalidDataException(String message) {
8+
super(message);
9+
}
10+
11+
public InvalidDataException(String message, NumericException e) {
12+
super(message, e);
13+
}
14+
}

connector/src/main/java/io/questdb/kafka/QuestDBSinkTask.java

Lines changed: 36 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -172,7 +172,22 @@ public void put(Collection<SinkRecord> collection) {
172172
if (httpTransport) {
173173
inflightSinkRecords.add(record);
174174
}
175-
handleSingleRecord(record);
175+
try {
176+
handleSingleRecord(record);
177+
} catch (InvalidDataException ex) {
178+
// data format error generated on client-side
179+
180+
if (httpTransport && reporter != null) {
181+
// we have DLQ set, let's report this single object
182+
183+
// remove the last item from in-flight records
184+
inflightSinkRecords.setPos(inflightSinkRecords.size() - 1);
185+
context.errantRecordReporter().report(record, ex);
186+
} else {
187+
// ok, no DQL, let's error the connector
188+
throw ex;
189+
}
190+
}
176191
}
177192

178193
if (httpTransport) {
@@ -257,7 +272,7 @@ private void onTcpSenderException(Exception e) {
257272
private void onHttpSenderException(Exception e) {
258273
closeSenderSilently();
259274
if (
260-
(reporter != null && e.getMessage() != null) // hack to detect data parsing errors
275+
(reporter != null && e.getMessage() != null) // hack to detect data parsing errors originating at server-side
261276
&& (e.getMessage().contains("error in line") || e.getMessage().contains("failed to parse line protocol"))
262277
) {
263278
// ok, we have a parsing error, let's try to send records one by one to find the problematic record
@@ -300,16 +315,27 @@ private void handleSingleRecord(SinkRecord record) {
300315
assert timestampColumnValue == Long.MIN_VALUE;
301316

302317
CharSequence tableName = recordToTable.apply(record);
318+
if (tableName == null || tableName.equals("")) {
319+
throw new InvalidDataException("Table name cannot be empty");
320+
}
303321
sender.table(tableName);
304322

305-
if (config.isIncludeKey()) {
306-
handleObject(config.getKeyPrefix(), record.keySchema(), record.key(), PRIMITIVE_KEY_FALLBACK_NAME);
323+
try {
324+
if (config.isIncludeKey()) {
325+
handleObject(config.getKeyPrefix(), record.keySchema(), record.key(), PRIMITIVE_KEY_FALLBACK_NAME);
326+
}
327+
handleObject(config.getValuePrefix(), record.valueSchema(), record.value(), PRIMITIVE_VALUE_FALLBACK_NAME);
328+
} catch (InvalidDataException ex) {
329+
if (httpTransport) {
330+
sender.cancelRow();
331+
}
332+
throw ex;
307333
}
308-
handleObject(config.getValuePrefix(), record.valueSchema(), record.value(), PRIMITIVE_VALUE_FALLBACK_NAME);
309334

310335
if (kafkaTimestampsEnabled) {
311336
timestampColumnValue = TimeUnit.MILLISECONDS.toNanos(record.timestamp());
312337
}
338+
313339
if (timestampColumnValue == Long.MIN_VALUE) {
314340
sender.atNow();
315341
} else {
@@ -338,7 +364,7 @@ private void handleMap(String name, Map<?, ?> value, String fallbackName) {
338364
for (Map.Entry<?, ?> entry : value.entrySet()) {
339365
Object mapKey = entry.getKey();
340366
if (!(mapKey instanceof String)) {
341-
throw new ConnectException("Map keys must be strings");
367+
throw new InvalidDataException("Map keys must be strings");
342368
}
343369
String mapKeyName = (String) mapKey;
344370
String entryName = name.isEmpty() ? mapKeyName : name + STRUCT_FIELD_SEPARATOR + mapKeyName;
@@ -365,7 +391,7 @@ private void handleObject(String name, Schema schema, Object value, String fallb
365391
if (isDesignatedColumnName(name, fallbackName)) {
366392
assert timestampColumnValue == Long.MIN_VALUE;
367393
if (value == null) {
368-
throw new ConnectException("Timestamp column value cannot be null");
394+
throw new InvalidDataException("Timestamp column value cannot be null");
369395
}
370396
timestampColumnValue = resolveDesignatedTimestampColumnValue(value, schema);
371397
return;
@@ -393,7 +419,7 @@ private long resolveDesignatedTimestampColumnValue(Object value, Schema schema)
393419
return parseToMicros((String) value) * 1000;
394420
}
395421
if (!(value instanceof Long)) {
396-
throw new ConnectException("Unsupported timestamp column type: " + value.getClass());
422+
throw new InvalidDataException("Unsupported timestamp column type: " + value.getClass());
397423
}
398424
long longValue = (Long) value;
399425
TimeUnit inputUnit;
@@ -453,7 +479,7 @@ private long parseToMicros(String timestamp) {
453479
try {
454480
return dataFormat.parse(timestamp, DateFormatUtils.EN_LOCALE);
455481
} catch (NumericException e) {
456-
throw new ConnectException("Cannot parse timestamp: " + timestamp + " with the configured format '" + config.getTimestampFormat() +"' use '"
482+
throw new InvalidDataException("Cannot parse timestamp: " + timestamp + " with the configured format '" + config.getTimestampFormat() +"' use '"
457483
+ QuestDBSinkConnectorConfig.TIMESTAMP_FORMAT + "' to configure the right timestamp format. " +
458484
"See https://questdb.io/docs/reference/function/date-time/#date-and-timestamp-format for timestamp parser documentation. ", e);
459485
}
@@ -513,7 +539,7 @@ private void onUnsupportedType(String name, Object type) {
513539
if (config.isSkipUnsupportedTypes()) {
514540
log.debug("Skipping unsupported type: {}, name: {}", type, name);
515541
} else {
516-
throw new ConnectException("Unsupported type: " + type + ", name: " + name);
542+
throw new InvalidDataException("Unsupported type: " + type + ", name: " + name);
517543
}
518544
}
519545

connector/src/test/java/io/questdb/kafka/QuestDBSinkConnectorEmbeddedTest.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@
5252
public final class QuestDBSinkConnectorEmbeddedTest {
5353
private static int httpPort = -1;
5454
private static int ilpPort = -1;
55-
private static final String OFFICIAL_QUESTDB_DOCKER = "questdb/questdb:8.1.1";
55+
private static final String OFFICIAL_QUESTDB_DOCKER = "questdb/questdb:8.2.0";
5656
private static final boolean DUMP_QUESTDB_CONTAINER_LOGS = true;
5757

5858
private EmbeddedConnectCluster connect;

pom.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@
8484
<dependency>
8585
<groupId>org.questdb</groupId>
8686
<artifactId>questdb</artifactId>
87-
<version>7.4.0</version>
87+
<version>8.2.0</version>
8888
</dependency>
8989
<dependency>
9090
<groupId>org.junit.jupiter</groupId>

0 commit comments

Comments
 (0)