Rename SCSt schema registry sample (#2500)

This commit is contained in:
Chris Bono
2022-08-29 19:30:14 -05:00
committed by GitHub
parent d38f57e06a
commit 449e6f83d8
22 changed files with 204 additions and 182 deletions

View File

@@ -22,7 +22,7 @@
</properties>
<modules>
<module>schema-registry-integration</module>
<module>spring-cloud-stream-schema-registry-integration</module>
<module>confluent-schema-registry-integration</module>
<module>kafka-streams-interactive-query</module>
</modules>

View File

@@ -1,134 +0,0 @@
== Spring Cloud Stream and Schema Evolution in Action with Kafka Binder.
These are a set of Spring Boot applications to demonstrate Schema Evolution using Spring Cloud Stream with Kafka binder.
Producer V1 (`producer1`), Producer V2 (`producer2`), and Consumer (`consumer`) are included in this project.
=== Requirement
As a developer, I'd like to design my consumer to be resilient to differing payload schemas.
=== Assumptions
For this demonstration, we will simply assume there are two producers producing events with different payload schemas.
A consumer that consumes both the payload versions will be designed to adapt to evolving schemas.
Both producers and consumers interact with the SCSt schema registry to register and evolve the schema.
[[build-apps]]
=== Building the applications
To build the applications simply execute the following command from the `scst-schema-registry-integration` directory:
[source,bash]
----
./mvnw clean install
----
NOTE: The apps can be built and run from w/in an IDE (such as IntelliJ) but you will need to invoke the Maven `package` goal and then `refresh` the project as the Avro Maven plugin needs to execute so that it generates the required model classes - otherwise you will see compile failures around missing `Sensor` class.
[[run-apps]]
=== Running the applications
==== Pre-requisites
****
* The components have all been built by following the <<build-apps>> steps.
* Apache Kafka broker available at `localhost:9092`
By default, the schema registry is backed by an H2 database. If you want to instead use Postgres, it must be available at `localhost:5432`. Similarly, if you want to use MySQL it must be available at `localhost:3306`.
TIP: The included link:../../../tools/kafka/docker-compose/README.adoc#_all_the_things[Kafka tools] can be used to easily start a broker locally on the required coordinates
****
==== Steps
Make sure the above pre-requisites are satisfied and that you are in the `scst-schema-registry-integration` directory and follow the steps below.
- Start the Schema Registry server (Change these commands accordingly if you are not on a Unix like platform)
[source,bash]
----
java -jar <ROOT-OF-SPRING-CLOUD-STREAM-CHECKOUT>/schema-registry/spring-cloud-stream-schema-registry-server-<VERSION>.jar
----
This starts the schema registry with a local H2 database.
To use Postgres instead of H2, additional properties must be specified when starting the server:
[source,bash]
----
java -jar <ROOT-OF-SPRING-CLOUD-STREAM-CHECKOUT>/schema-registry/spring-cloud-stream-schema-registry-server-<VERSION>.jar \
--spring.datasource.url=jdbc:postgresql://localhost:5432/registry \
--spring.datasource.username=root \
--spring.datasource.password=rootpw \
--spring.datasource.driver-class-name=org.postgresql.Driver \
--spring.jpa.database-platform=org.hibernate.dialect.PostgreSQLDialect \
--spring.jpa.hibernate.ddl-auto-create=true \
--spring.jpa.hibernate.ddl-auto=update \
--spring.jpa.generate-ddl=true
----
To use MySQL database instead of H2, additional properties must be specified when starting the server:
[source,bash]
----
java -jar <ROOT-OF-SPRING-CLOUD-STREAM-CHECKOUT>/schema-registry/spring-cloud-stream-schema-registry-server-<VERSION>.jar \
--spring.datasource.url=jdbc:mariadb://localhost:3306/registry \
--spring.datasource.username=root \
--spring.datasource.password=rootpw \
--spring.datasource.driver-class-name=org.mariadb.jdbc.Driver \
--spring.jpa.database-platform=org.hibernate.dialect.MariaDB53Dialect \
--spring.jpa.hibernate.ddl-auto-create=true \
--spring.jpa.hibernate.ddl-auto=update \
--spring.jpa.generate-ddl=true
----
- Start `consumer` on another terminal session (or run it from an IDE)
[source,bash]
----
java -jar scst-schema-registry-consumer-kafka/target/scst-schema-registry-consumer-kafka-<VERSION>-SNAPSHOT.jar
----
- Start `producer1` on another terminal session (or run it from an IDE)
[source,bash]
----
java -jar scst-schema-registry-producer1-kafka/target/scst-schema-registry-producer1-kafka-<VERSION>-SNAPSHOT.jar
----
- Start `producer2` on another terminal session (or run it from an IDE)
[source,bash]
----
java -jar scst-schema-registry-producer2-kafka/target/scst-schema-registry-producer2-kafka-<VERSION>-SNAPSHOT.jar
----
=== Sample Data
Both the producers in the demonstration are _also_ REST controllers. We will hit the `/messages` endpoint on each producer
to POST sample data.
_Example:_
[source,bash]
----
curl -X POST http://localhost:9009/messages
curl -X POST http://localhost:9010/messages
curl -X POST http://localhost:9009/messages
curl -X POST http://localhost:9009/messages
curl -X POST http://localhost:9010/messages
----
=== Output
The consumer should log the results.
[source,bash,options=nowrap,subs=attributes]
----
{"id": "d135efc3-72f8-4612-9497-184cae508e31-v1", "internalTemperature": 34.36362, "externalTemperature": 0.0, "acceleration": 9.656547, "velocity": 33.29733}
{"id": "3ecaae18-3144-4570-800a-223ca3198001-v1", "internalTemperature": 28.410656, "externalTemperature": 0.0, "acceleration": 1.752817, "velocity": 69.82016}
{"id": "81262d85-2d60-40e8-9fcc-7f797fd6dcd4-v2", "internalTemperature": 11.2908125, "externalTemperature": 26.260101, "acceleration": 3.268205, "velocity": 3.331542}
----
NOTE: Refer to the payload suffix in the `id` field. Each of them are appended with `-v1` or `-v2` indicating they are from
`producer1` and `producer2` respectively.
=== What just happened?
The schema evolved on the `temperature` field. That field is now split into `internalTemperature` and `externalTemperature`,
as two separate fields. The `producer1` produces payload only with `temperature` and on the other hand, `producer2` produces
payload with `internalTemperature` and `externalTemperature` fields in it.
The `consumer` is coded against a base schema that include the split fields.
The `consumer` app can happily deserialize the payload with `internalTemperature` and `externalTemperature` fields. However, when
a `producer1` payload arrives (which includes `temperature` field), the schema evolution and compatibility check are automatically
applied.
Because each payload also includes the payload version in the header, Spring Cloud Stream with the help of Schema Registry server and Avro, the schema evolution occurs behind the scenes.
The automatic mapping of `temperature` to `internalTemperature` field is applied, since that's the field where the `aliases` is defined.

View File

@@ -0,0 +1,174 @@
== Spring Cloud Stream and Schema Evolution in Action with Kafka Binder.
:project-version: 4.0.0-SNAPSHOT
These are a set of Spring Boot applications to demonstrate Schema Evolution using Spring Cloud Stream with Kafka binder.
Producer V1 (`producer1`), Producer V2 (`producer2`), and Consumer (`consumer`) are included in this project.
=== Requirement
As a developer, I'd like to design my consumer to be resilient to differing payload schemas.
=== Assumptions
For this demonstration, we will simply assume there are two producers producing events with different payload schemas.
A consumer that consumes both the payload versions will be designed to adapt to evolving schemas.
Both producers and consumers interact with the SCSt schema registry to register and evolve the schema.
[[build-apps]]
=== Building
====
NOTE: It is expected that this repo has been checked out locally and All commands are executed from this sample's directory `spring-cloud-stream-schema-registry-integration` unless otherwise noted.
====
==== Build the schema registry
The schema registry must be built by executing the following command:
[source,bash]
----
pushd ../../schema-registry
../mvnw clean install
popd
----
==== Build the apps
To build the applications simply execute the following command:
[source,bash]
----
./mvnw clean install
----
NOTE: The apps can be built and run from w/in an IDE (such as IntelliJ) but you will need to invoke the Maven `package` goal and then `refresh` the project as the Avro Maven plugin needs to execute so that it generates the required model classes - otherwise you will see compile failures around missing `Sensor` class.
[[run-apps]]
=== Running
====
NOTE: It is expected that this repo has been checked out locally and All commands are executed from this sample's directory `spring-cloud-stream-schema-registry-integration` unless otherwise noted.
====
==== Pre-requisites
****
* The components have all been built by following the <<build-apps>> steps.
* Apache Kafka broker available at `localhost:9092`
TIP: The included link:../../../tools/kafka/docker-compose/README.adoc#_all_the_things[Kafka tools] can be used to easily start a broker locally on the required coordinates
* By default, the schema registry is backed by an `H2` database.
** To instead use `Postgres` it must be available at `localhost:5432`
** To instead use `MySQL` it must be available at `localhost:3306`.
TIP: Docker compose files are provided for both link:./postgres.yml[Postgres] and link:./mysql.yml[MySQL]. You can simply run `docker-compose -f <postgres.yml|mysql.yml> <up|down>` to start/stop the database server
****
==== Steps
Make sure the above pre-requisites are satisfied and follow the steps below.
===== Start Schema Registry
Start the Schema Registry server (_adjust commands accordingly if you are not on a Unix like platform_)
[source,bash,subs="attributes"]
----
java -jar ../../schema-registry/spring-cloud-stream-schema-registry-server/target/spring-cloud-stream-schema-registry-server-4.0.0-SNAPSHOT.jar
----
By default the schema registry starts with a local `H2` database.
.To use `Postgres` database instead of `H2`...
[%collapsible]
====
additional properties must be specified when starting the server:
[source,bash,subs="attributes"]
----
java -jar ../../schema-registry/spring-cloud-stream-schema-registry-server/target/spring-cloud-stream-schema-registry-server-{project-version}.jar \
--spring.datasource.url=jdbc:postgresql://localhost:5432/registry \
--spring.datasource.username=root \
--spring.datasource.password=rootpw \
--spring.datasource.driver-class-name=org.postgresql.Driver \
--spring.jpa.database-platform=org.hibernate.dialect.PostgreSQLDialect \
--spring.jpa.hibernate.ddl-auto-create=true \
--spring.jpa.hibernate.ddl-auto=update \
--spring.jpa.generate-ddl=true
----
====
.To use `MySQL` database instead of `H2`...
[%collapsible]
====
additional properties must be specified when starting the server:
[source,bash,subs="attributes"]
----
java -jar ../../schema-registry/spring-cloud-stream-schema-registry-server/target/spring-cloud-stream-schema-registry-server-{project-version}.jar \
--spring.datasource.url=jdbc:mariadb://localhost:3306/registry \
--spring.datasource.username=root \
--spring.datasource.password=rootpw \
--spring.datasource.driver-class-name=org.mariadb.jdbc.Driver \
--spring.jpa.database-platform=org.hibernate.dialect.MariaDB53Dialect \
--spring.jpa.hibernate.ddl-auto-create=true \
--spring.jpa.hibernate.ddl-auto=update \
--spring.jpa.generate-ddl=true
----
====
===== Start consumer
Start `consumer` on another terminal session (or run it from an IDE)
[source,bash,subs="attributes"]
----
java -jar schema-registry-consumer-kafka/target/schema-registry-consumer-kafka-{project-version}.jar
----
===== Start V1 producer
Start `producer1` on another terminal session (or run it from an IDE)
[source,bash,subs="attributes"]
----
java -jar schema-registry-producer1-kafka/target/schema-registry-producer1-kafka-{project-version}.jar
----
===== Start V2 producer
Start `producer2` on another terminal session (or run it from an IDE)
[source,bash,subs="attributes"]
----
java -jar schema-registry-producer2-kafka/target/schema-registry-producer2-kafka-{project-version}.jar
----
=== Sample Data
Both the producers in the demonstration are _also_ REST controllers. We will hit the `/randomMessage` endpoint on each producer
to POST sample data.
_Example:_
[source,bash]
----
curl -X POST http://localhost:9009/randomMessage
curl -X POST http://localhost:9010/randomMessage
curl -X POST http://localhost:9009/randomMessage
curl -X POST http://localhost:9009/randomMessage
curl -X POST http://localhost:9010/randomMessage
----
=== Output
The consumer should log the results.
[source,bash,options=nowrap,subs=attributes]
----
{"id": "d5657e55-c2cd-48f0-a22e-d28d1ef10873-v1", "internalTemperature": 19.534815, "externalTemperature": 0.0, "acceleration": 5.286502, "velocity": 25.349945}
{"id": "6a6de265-997c-4bf9-8eae-97accccb78e9-v2", "internalTemperature": 39.443855, "externalTemperature": 40.365253, "acceleration": 1.8879288, "velocity": 2.5296867}
{"id": "f09defad-828f-43ae-93a4-e777754cf57a-v1", "internalTemperature": 15.895501, "externalTemperature": 0.0, "acceleration": 1.9341749, "velocity": 52.868507}
{"id": "b39b8c73-eec3-4abd-b8d2-cc405eb39bd7-v1", "internalTemperature": 44.90698, "externalTemperature": 0.0, "acceleration": 1.5393275, "velocity": 87.0358}
{"id": "19d5c20e-ec18-4b35-a82a-c8322d7fea27-v2", "internalTemperature": 19.203693, "externalTemperature": 47.290142, "acceleration": 1.125809, "velocity": 11.153614}
----
NOTE: Refer to the payload suffix in the `id` field. Each of them are appended with `-v1` or `-v2` indicating they are from
`producer1` and `producer2` respectively.
=== What just happened?
The schema evolved on the `temperature` field. That field is now split into `internalTemperature` and `externalTemperature`,
as two separate fields. The `producer1` produces payload only with `temperature` and on the other hand, `producer2` produces
payload with `internalTemperature` and `externalTemperature` fields in it.
The `consumer` is coded against a base schema that include the split fields.
The `consumer` app can happily deserialize the payload with `internalTemperature` and `externalTemperature` fields. However, when
a `producer1` payload arrives (which includes `temperature` field), the schema evolution and compatibility check are automatically
applied.
Because each payload also includes the payload version in the header, Spring Cloud Stream with the help of Schema Registry server and Avro, the schema evolution occurs behind the scenes.
The automatic mapping of `temperature` to `internalTemperature` field is applied, since that's the field where the `aliases` is defined.

View File

@@ -1,10 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<artifactId>scst-schema-registry-integration</artifactId>
<artifactId>spring-cloud-stream-schema-registry-integration</artifactId>
<version>4.0.0-SNAPSHOT</version>
<packaging>pom</packaging>
<name>scst-schema-registry-integration</name>
<name>spring-cloud-stream-schema-registry-integration</name>
<description>SCSt Schema Registry Integration Sample App</description>
<parent>

View File

@@ -2,15 +2,15 @@
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<artifactId>scst-schema-registry-consumer-kafka</artifactId>
<artifactId>schema-registry-consumer-kafka</artifactId>
<version>4.0.0-SNAPSHOT</version>
<packaging>jar</packaging>
<name>scst-schema-registry-consumer-kafka</name>
<name>schema-registry-consumer-kafka</name>
<description>SCSt Schema Registry Consumer</description>
<parent>
<groupId>org.springframework.cloud</groupId>
<artifactId>scst-schema-registry-integration</artifactId>
<artifactId>spring-cloud-stream-schema-registry-integration</artifactId>
<version>4.0.0-SNAPSHOT</version>
</parent>

View File

@@ -2,15 +2,15 @@
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<artifactId>scst-schema-registry-producer1-kafka</artifactId>
<artifactId>schema-registry-producer1-kafka</artifactId>
<version>4.0.0-SNAPSHOT</version>
<packaging>jar</packaging>
<name>scst-schema-registry-producer1-kafka</name>
<name>schema-registry-producer1-kafka</name>
<description>SCSt Schema Registry Producer 1</description>
<parent>
<groupId>org.springframework.cloud</groupId>
<artifactId>scst-schema-registry-integration</artifactId>
<artifactId>spring-cloud-stream-schema-registry-integration</artifactId>
<version>4.0.0-SNAPSHOT</version>
</parent>

View File

@@ -22,12 +22,18 @@ public class Producer1ApplicationKafka {
private Random random = new Random();
@Autowired
StreamBridge streamBridge;
private StreamBridge streamBridge;
public static void main(String[] args) {
SpringApplication.run(Producer1ApplicationKafka.class, args);
}
@RequestMapping(value = "/randomMessage", method = RequestMethod.POST)
public String sendRandomMessage() {
streamBridge.send("supplier-out-0", randomSensor());
return "ok, have fun with v1 payload!";
}
private Sensor randomSensor() {
Sensor sensor = new Sensor();
sensor.setId(UUID.randomUUID() + "-v1");
@@ -36,12 +42,4 @@ public class Producer1ApplicationKafka {
sensor.setTemperature(random.nextFloat() * 50);
return sensor;
}
@RequestMapping(value = "/messages", method = RequestMethod.POST)
public String sendMessage() {
streamBridge.send("supplier-out-0", randomSensor());
return "ok, have fun with v1 payload!";
}
}

View File

@@ -2,15 +2,15 @@
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<artifactId>scst-schema-registry-producer2-kafka</artifactId>
<artifactId>schema-registry-producer2-kafka</artifactId>
<version>4.0.0-SNAPSHOT</version>
<packaging>jar</packaging>
<name>scst-schema-registry-producer2-kafka</name>
<name>schema-registry-producer2-kafka</name>
<description>SCSt Schema Registry Producer 2</description>
<parent>
<groupId>org.springframework.cloud</groupId>
<artifactId>scst-schema-registry-integration</artifactId>
<artifactId>spring-cloud-stream-schema-registry-integration</artifactId>
<version>4.0.0-SNAPSHOT</version>
</parent>

View File

@@ -1,13 +1,17 @@
package sample.producer2;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.stream.function.StreamBridge;
import org.springframework.cloud.stream.schema.registry.client.EnableSchemaRegistryClient;
import org.springframework.context.annotation.Bean;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
import java.util.Collections;
import java.util.Random;
import java.util.UUID;
import java.util.concurrent.BlockingQueue;
@@ -23,23 +27,19 @@ public class Producer2ApplicationKafka {
private Random random = new Random();
BlockingQueue<Sensor> unbounded = new LinkedBlockingQueue<>();
@Autowired
private StreamBridge streamBridge;
public static void main(String[] args) {
SpringApplication.run(Producer2ApplicationKafka.class, args);
}
@RequestMapping(value = "/messages", method = RequestMethod.POST)
public String sendMessage() {
unbounded.offer(randomSensor());
@PostMapping("/randomMessage")
public String sendRandomMessage() {
streamBridge.send("supplier-out-0", randomSensor());
return "ok, have fun with v2 payload!";
}
@Bean
public Supplier<Sensor> supplier() {
return () -> unbounded.poll();
}
private Sensor randomSensor() {
Sensor sensor = new Sensor();
sensor.setId(UUID.randomUUID().toString() + "-v2");
@@ -47,9 +47,6 @@ public class Producer2ApplicationKafka {
sensor.setVelocity(random.nextFloat() * 100);
sensor.setInternalTemperature(random.nextFloat() * 50);
sensor.setExternalTemperature(random.nextFloat() * 50);
sensor.setAccelerometer(null);
sensor.setMagneticField(null);
return sensor;
}
}

View File

@@ -7,19 +7,6 @@
{"name":"internalTemperature", "type":"float", "default":0.0, "aliases":["temperature"]},
{"name":"externalTemperature", "type":"float", "default":0.0},
{"name":"acceleration", "type":"float","default":0.0},
{"name":"velocity","type":"float","default":0.0},
{"name":"accelerometer","type":[
"null",{
"type":"array",
"items":"float"
}
]},
{"name":"magneticField","type":[
"null",{
"type":"array",
"items":"float"
}
]}
{"name":"velocity","type":"float","default":0.0}
]
}
}