-
Notifications
You must be signed in to change notification settings - Fork 12
feat: error handling in sink connector #142
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 1 commit
d4a6105
ac2844e
b602638
6c314e1
8fdcdaf
5ca3cc5
ed332d7
286f095
98c246d
0fb4321
6e8cea6
113b92d
b9b9ffc
0984c7b
65300e8
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -113,7 +113,7 @@ abstract class Neo4jSinkErrorIT { | |
| errorDlqTopic = DLQ_TOPIC, | ||
| errorDlqContextHeadersEnable = true) | ||
| @Test | ||
| fun `should report an error with all error headers when headers enabled`( | ||
| fun `should report an error with all error headers when headers are enabled`( | ||
| @TopicProducer(TOPIC) producer: ConvertingKafkaProducer, | ||
| @TopicConsumer(topic = DLQ_TOPIC, offset = "earliest") consumer: ConvertingKafkaConsumer, | ||
| session: Session | ||
|
|
@@ -162,7 +162,7 @@ abstract class Neo4jSinkErrorIT { | |
| errorDlqTopic = DLQ_TOPIC, | ||
| errorDlqContextHeadersEnable = true) | ||
| @Test | ||
| fun `should report errors when cypher strategy with multiple events`( | ||
| fun `should report failed events with cypher strategy`( | ||
| @TopicProducer(TOPIC) producer: ConvertingKafkaProducer, | ||
| @TopicConsumer(topic = DLQ_TOPIC, offset = "earliest") consumer: ConvertingKafkaConsumer, | ||
| session: Session, | ||
|
|
@@ -244,7 +244,7 @@ abstract class Neo4jSinkErrorIT { | |
| errorDlqTopic = DLQ_TOPIC, | ||
| errorDlqContextHeadersEnable = true) | ||
| @Test | ||
| fun `should report errors when node pattern strategy with multiple events`( | ||
| fun `should report failed events with node pattern strategy`( | ||
| @TopicProducer(TOPIC) producer: ConvertingKafkaProducer, | ||
| @TopicConsumer(topic = DLQ_TOPIC, offset = "earliest") consumer: ConvertingKafkaConsumer, | ||
| session: Session, | ||
|
|
@@ -297,9 +297,9 @@ abstract class Neo4jSinkErrorIT { | |
| val errorHeaders = ErrorHeaders(it.raw.headers()) | ||
| errorHeaders.getValue(ErrorHeaders.OFFSET) shouldBe 3 | ||
| errorHeaders.getValue(ErrorHeaders.EXCEPTION_CLASS_NAME) shouldBe | ||
| "org.neo4j.driver.exceptions.ClientException" | ||
| "org.neo4j.connectors.kafka.exceptions.InvalidDataException" | ||
| errorHeaders.getValue(ErrorHeaders.EXCEPTION_MESSAGE) shouldBe | ||
| "Unable to convert kotlin.Unit to Neo4j Value." | ||
| "Key 'id' could not be located in the message." | ||
|
|
||
| it.value shouldBe message4ToFail.value | ||
| } | ||
|
|
@@ -317,7 +317,7 @@ abstract class Neo4jSinkErrorIT { | |
| errorDlqTopic = DLQ_TOPIC, | ||
| errorDlqContextHeadersEnable = true) | ||
| @Test | ||
| fun `should report errors when relationship pattern strategy with multiple events`( | ||
| fun `should report failed events with relationship pattern strategy`( | ||
| @TopicProducer(TOPIC) producer: ConvertingKafkaProducer, | ||
| @TopicConsumer(topic = DLQ_TOPIC, offset = "earliest") consumer: ConvertingKafkaConsumer, | ||
| session: Session, | ||
|
|
@@ -418,9 +418,9 @@ abstract class Neo4jSinkErrorIT { | |
| val errorHeaders = ErrorHeaders(it.raw.headers()) | ||
| errorHeaders.getValue(ErrorHeaders.OFFSET) shouldBe 4 | ||
| errorHeaders.getValue(ErrorHeaders.EXCEPTION_CLASS_NAME) shouldBe | ||
| "org.neo4j.driver.exceptions.ClientException" | ||
| "org.neo4j.connectors.kafka.exceptions.InvalidDataException" | ||
| errorHeaders.getValue(ErrorHeaders.EXCEPTION_MESSAGE) shouldBe | ||
| "Unable to convert kotlin.Unit to Neo4j Value." | ||
| "Key 'itemId' could not be located in the message." | ||
|
|
||
| it.value shouldBe message5ToFail.value | ||
| } | ||
|
|
@@ -430,7 +430,7 @@ abstract class Neo4jSinkErrorIT { | |
| @Neo4jSink( | ||
| cud = [CudStrategy(TOPIC)], errorDlqTopic = DLQ_TOPIC, errorDlqContextHeadersEnable = true) | ||
|
||
| @Test | ||
| fun `should report errors when cud strategy with multiple events`( | ||
| fun `should report failed events with cud strategy`( | ||
| @TopicProducer(TOPIC) producer: ConvertingKafkaProducer, | ||
| @TopicConsumer(topic = DLQ_TOPIC, offset = "earliest") consumer: ConvertingKafkaConsumer, | ||
| session: Session, | ||
|
|
@@ -548,7 +548,7 @@ abstract class Neo4jSinkErrorIT { | |
| errorDlqTopic = DLQ_TOPIC, | ||
| errorDlqContextHeadersEnable = true) | ||
| @Test | ||
| fun `should report errors when cdc schema strategy with multiple events`( | ||
| fun `should report failed events with cdc schema strategy`( | ||
| @TopicProducer(TOPIC) producer: ConvertingKafkaProducer, | ||
| @TopicConsumer(DLQ_TOPIC, offset = "earliest") consumer: ConvertingKafkaConsumer, | ||
| session: Session | ||
|
|
@@ -661,7 +661,7 @@ abstract class Neo4jSinkErrorIT { | |
| errorDlqTopic = DLQ_TOPIC, | ||
| errorDlqContextHeadersEnable = true) | ||
| @Test | ||
| fun `should report errors when cdc source id strategy with multiple events`( | ||
| fun `should report failed events with cdc source id strategy`( | ||
| @TopicProducer(TOPIC) producer: ConvertingKafkaProducer, | ||
| @TopicConsumer(DLQ_TOPIC, offset = "earliest") consumer: ConvertingKafkaConsumer, | ||
| session: Session | ||
|
|
@@ -782,7 +782,7 @@ abstract class Neo4jSinkErrorIT { | |
| errorTolerance = "none", | ||
| errorDlqTopic = DLQ_TOPIC) | ||
| @Test | ||
| fun `should report an error when tolerance none with multiple events`( | ||
| fun `should stop the process and only report first failed event when error tolerance is none`( | ||
| @TopicProducer(TOPIC) producer: ConvertingKafkaProducer, | ||
| @TopicConsumer(topic = DLQ_TOPIC, offset = "earliest") consumer: ConvertingKafkaConsumer, | ||
| session: Session, | ||
|
|
@@ -798,7 +798,7 @@ abstract class Neo4jSinkErrorIT { | |
| val message3 = | ||
| KafkaMessage( | ||
| valueSchema = Schema.STRING_SCHEMA, | ||
| value = """{"id": 3, "name": "Mary", "surname": "Doe"}""") | ||
| value = """{"id": 3, name: "Mary", "surname": "Doe"}""") | ||
|
|
||
| producer.publish(message1, message2ToFail, message3) | ||
|
|
||
|
|
@@ -829,7 +829,7 @@ abstract class Neo4jSinkErrorIT { | |
| errorDlqTopic = DLQ_TOPIC, | ||
| errorDlqContextHeadersEnable = true) | ||
| @Test | ||
| fun `should be able to report from different topics with headers`( | ||
| fun `should report failed events from different topics`( | ||
| @TopicProducer(TOPIC_1) producer1: ConvertingKafkaProducer, | ||
| @TopicProducer(TOPIC_2) producer2: ConvertingKafkaProducer, | ||
| @TopicProducer(TOPIC_3) producer3: ConvertingKafkaProducer, | ||
|
|
@@ -885,9 +885,9 @@ abstract class Neo4jSinkErrorIT { | |
| val errorHeaders = ErrorHeaders(it.raw.headers()) | ||
| errorHeaders.getValue(ErrorHeaders.TOPIC) shouldBe producer3.topic | ||
| errorHeaders.getValue(ErrorHeaders.EXCEPTION_CLASS_NAME) shouldBe | ||
| "org.neo4j.driver.exceptions.ClientException" | ||
| "org.neo4j.connectors.kafka.exceptions.InvalidDataException" | ||
| errorHeaders.getValue(ErrorHeaders.EXCEPTION_MESSAGE) shouldBe | ||
| "Unable to convert kotlin.Unit to Neo4j Value." | ||
| "Key 'id' could not be located in the message." | ||
|
|
||
| it.value shouldBe nodePatternMessageToFail.value | ||
| } | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -18,6 +18,7 @@ package org.neo4j.connectors.kafka.sink.strategy | |
|
|
||
| import org.neo4j.cdc.client.model.NodeEvent | ||
| import org.neo4j.cdc.client.model.RelationshipEvent | ||
| import org.neo4j.connectors.kafka.exceptions.InvalidDataException | ||
| import org.neo4j.connectors.kafka.sink.SinkStrategy | ||
| import org.neo4j.cypherdsl.core.Cypher | ||
| import org.neo4j.cypherdsl.core.Node | ||
|
|
@@ -30,6 +31,10 @@ class CdcSchemaHandler(val topic: String, private val renderer: Renderer) : CdcH | |
| override fun strategy() = SinkStrategy.CDC_SCHEMA | ||
|
|
||
| override fun transformCreate(event: NodeEvent): Query { | ||
| if (event.after == null) { | ||
| throw InvalidDataException("create operation requires 'after' field in the event object") | ||
| } | ||
|
|
||
| val node = buildNode(event.keys, "n") | ||
| val stmt = | ||
| Cypher.merge(node) | ||
|
|
@@ -49,10 +54,10 @@ class CdcSchemaHandler(val topic: String, private val renderer: Renderer) : CdcH | |
|
|
||
| override fun transformUpdate(event: NodeEvent): Query { | ||
| if (event.before == null) { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. should we also have a similar validation for create and delete events, maybe in a follow-up PR. wdyt?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. yes, I think we should, since they can also fail if before or after is null |
||
| throw IllegalArgumentException("update operation requires 'before' field in the event object") | ||
| throw InvalidDataException("update operation requires 'before' field in the event object") | ||
| } | ||
| if (event.after == null) { | ||
| throw IllegalArgumentException("update operation requires 'after' field in the event object") | ||
| throw InvalidDataException("update operation requires 'after' field in the event object") | ||
| } | ||
|
|
||
| val node = buildNode(event.keys, "n") | ||
|
|
@@ -88,6 +93,10 @@ class CdcSchemaHandler(val topic: String, private val renderer: Renderer) : CdcH | |
| } | ||
|
|
||
| override fun transformCreate(event: RelationshipEvent): Query { | ||
| if (event.after == null) { | ||
| throw InvalidDataException("create operation requires 'after' field in the event object") | ||
| } | ||
|
|
||
| val (start, end, rel) = buildRelationship(event, "r") | ||
| val stmt = | ||
| Cypher.merge(start) | ||
|
|
@@ -101,10 +110,10 @@ class CdcSchemaHandler(val topic: String, private val renderer: Renderer) : CdcH | |
|
|
||
| override fun transformUpdate(event: RelationshipEvent): Query { | ||
| if (event.before == null) { | ||
| throw IllegalArgumentException("update operation requires 'before' field in the event object") | ||
| throw InvalidDataException("update operation requires 'before' field in the event object") | ||
| } | ||
| if (event.after == null) { | ||
| throw IllegalArgumentException("update operation requires 'after' field in the event object") | ||
| throw InvalidDataException("update operation requires 'after' field in the event object") | ||
| } | ||
|
|
||
| val (start, end, rel) = buildRelationship(event, "r") | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.