Files
spring-pulsar/spring-pulsar-sample-apps/sample-pulsar-functions
Chris Bono ae05982286 Update to Pulsar 3.3.2 (#875)
This commit updates the version of Pulsar to 3.3.2.

Also, in Pulsar 3.3.2 the schema validation of an outgoing message value
happens later than it did previously. This requires the PulsarTemplate
to widen the try/catch net so that when this happens the producer is
closed properly. The backing change in Pulsar 3.3.2 can be seen here
f3c177e224 (diff-095bc2359e03726e031d8c2f210560c6a3218f48a969b0f75a2482e25cd54744R69)
2024-10-16 10:24:01 -05:00
..

= Sample Stream Pipeline with Pulsar Functions

:curdir: {docdir}/guides/pulsar-functions

A sample appfootnote:[Inspired by the sample app in https://streamnative.io/blog/release/2022-09-21-announcing-spring-for-apache-pulsar/] that leverages the built-in Pulsar Functions support in Spring for Apache Pulsar to create a streaming pipeline to handle user signups.

The app consists of a Rabbit source, a Pulsar function, and a Cassandra sink with the following details:

* Messages sent to RabbitMQ `user_signup` queue are sourced into Pulsar `user-signup` topic
* Messages from `user-signup` topic are fed into Pulsar Signup function which:
** the signup tier count is incremented and logged
** `ENTERPRISE` tier signups result in a customer message in `customer-onboard` Pulsar topic
* Messages from `customer-onboard` topic are sinked into `customer_onboard` Cassandra table

== Pre-requisites
****
* Ability to run Docker containers locally and Docker Compose installed - see https://docs.docker.com/compose/install/[here] for more details
****

== Steps

****
NOTE: All commands are expected to be executed from the directory this document lives in [.small]#link:{curdir}[]#
****

=== Build app and function
Build the application and function libs with the following command:
----
../../gradlew clean build
----

=== Download connectors
The connector libs are not included in the Docker image, download them one time by executing the following script:

[source,shell]
----
./download-connectors.sh
----

=== Start services
Start the Pulsar, RabbitMQ, and Cassandra services using Docker Compose with the following command:
----
docker-compose up -d
----

.Verify `rabbitmq` is ready by executing the following command
----
docker logs rabbitmq | grep "Server startup complete"
----
.which should produce
----
2023-01-04 21:06:39.692935+00:00 [info] <0.721.0> Server startup complete; 4 plugins started.
----

.Verify `pulsar` is ready by executing the following command
----
docker logs pulsar 2>&1 | grep "messaging service is ready"
----
.which should produce
----
2023-01-04T21:39:57 [main] INFO  org.apache.pulsar.broker.PulsarService - messaging service is ready, bootstrap_seconds=4
2023-01-04T21:39:57 [main] INFO  org.apache.pulsar.broker.PulsarService - messaging service is ready, bootstrap service port = 8080, broker url= pulsar://localhost:6650, cluster=standalone, configs=org.apache.pulsar.broker....
----

.Verify `pulsar` has the connectors installed by executing the following command
----
curl -s http://localhost:8080/admin/v2/functions/connectors
----
.which should produce
[source,json]
----
[
  {
    "name": "cassandra",
    "description": "Writes data into Cassandra",
    "sinkClass": "org.apache.pulsar.io.cassandra.CassandraStringSink",
    "sinkConfigClass": "org.apache.pulsar.io.cassandra.CassandraSinkConfig"
  },
  {
    "name": "rabbitmq",
    "description": "RabbitMQ source and sink connector",
    "sourceClass": "org.apache.pulsar.io.rabbitmq.RabbitMQSource",
    "sinkClass": "org.apache.pulsar.io.rabbitmq.RabbitMQSink",
    "sourceConfigClass": "org.apache.pulsar.io.rabbitmq.RabbitMQSourceConfig",
    "sinkConfigClass": "org.apache.pulsar.io.rabbitmq.RabbitMQSinkConfig"
  }
]
----

.Verify `cassandra` is ready by executing the following command
----
docker logs cassandra 2>&1 | grep "Creating replication strategy sample_pulsar_functions_keyspace params KeyspaceParams"
----
.which should produce
----
INFO  [Native-Transport-Requests-5] 2023-01-04 21:40:48,635 Keyspace.java:381 - Creating replication strategy sample_pulsar_functions_keyspace params KeyspaceParams{durable_writes=true, replication=ReplicationParams{class=org.apache.cassandra.locator.SimpleStrategy, replication_factor=1}}
----
NOTE: It will typically take about 60 seconds for `cassandra` to complete its startup process so if the above fails, try again after 60 seconds.

At this point the following services are up and running:

* `cassandra`
** **keyspace:** `sample_pulsar_functions_keyspace`
** **table:** `customer_onboard`
* `rabbitmq`
** management UI
* `pulsar` standalone
** function support enabled
** `cassandra` and `rabbit` connectors installed

=== Start application
The sample app registers the Pulsar Functions which effectively create the streaming pipeline.

.Run the sample app within your IDE or by executing the following command
----
cd sample-signup-app && ../../../gradlew bootRun
----
.which should produce
----
[main] PulsarFunctionAdministration : Creating 'UserSignupFunction' function (using local archive: /Users/cbono/repos/spring-pulsar/spring-pulsar-sample-apps/sample-pulsar-functions/signup-function/target/signup-function-0.0.1-SNAPSHOT.jar)
[main] PulsarFunctionAdministration : Creating 'CustomerOnboardCassandraSink' sink (using local archive: builtin://cassandra)
[main] PulsarFunctionAdministration : Creating 'UserSignupRabbitSource' source (using local archive: builtin://rabbitmq)
[main] SignupApplication            : Started SignupApplication in 6.485 seconds (process running for 6.839)
----

.Verify the functions are actually registered by executing the following Pulsar commands
----
docker exec -it pulsar bin/pulsar-admin sources list
docker exec -it pulsar bin/pulsar-admin functions list
docker exec -it pulsar bin/pulsar-admin sinks list
----
.which should produce
----
[
  "UserSignupRabbitSource"
]
UserSignupFunction
[
  "CustomerOnboardCassandraSink"
]
----

=== Verify pipeline
The app produces a random user signup record to the RabbitMQ `user_signup` queue every 5 seconds.
It also logs all messages on the `user-signup` and `customer-onboard` Pulsar topics as well as the last 5 emails sent to the `customer_onboard` Cassandra table.

To verify the pipeline is working simply watch the console log as the app runs. 
The output should look like similar to the following:
----
TO RABBIT user_signup => Signup[signupTier=ENTERPRISE, firstName=Samuel, lastName=Weiss, email=samuel.weiss@robutenia.eu, signupTimestamp=1673236049021]
FROM PULSAR user-signup => Signup[signupTier=ENTERPRISE, firstName=Samuel, lastName=Weiss, email=samuel.weiss@robutenia.eu, signupTimestamp=1673236049021]
FROM PULSAR customer-onboard => Customer[firstName=Samuel, lastName=Weiss, email=samuel.weiss@robutenia.eu, signupTimestamp=1673236049021]
FROM CASSANDRA => latest (5/18) emails: carson.maddox@interdemconsulting.biz, aria.burke@interdemassociates.biz, layla.burks@memortech.com, joshua.chandler@furbainc.com, abigail.cooley@quickerinc.com...

TO RABBIT user_signup => Signup[signupTier=BASIC, firstName=Arianna, lastName=Edwards, email=arianna.edwards@robutenia.eu, signupTimestamp=1673236054031]
FROM PULSAR user-signup => Signup[signupTier=BASIC, firstName=Arianna, lastName=Edwards, email=arianna.edwards@robutenia.eu, signupTimestamp=1673236054031]

TO RABBIT user_signup => Signup[signupTier=STANDARD, firstName=Kylie, lastName=Raymond, email=kylie.raymond@yrsa.eu, signupTimestamp=1673236059038]
FROM PULSAR user-signup => Signup[signupTier=STANDARD, firstName=Kylie, lastName=Raymond, email=kylie.raymond@yrsa.eu, signupTimestamp=1673236059038]

TO RABBIT user_signup => Signup[signupTier=ENTERPRISE, firstName=Nolan, lastName=Floyd, email=nolan.floyd@flyhighassociates.eu, signupTimestamp=1673236064045]
FROM PULSAR user-signup => Signup[signupTier=ENTERPRISE, firstName=Nolan, lastName=Floyd, email=nolan.floyd@flyhighassociates.eu, signupTimestamp=1673236064045]
FROM PULSAR customer-onboard => Customer[firstName=Nolan, lastName=Floyd, email=nolan.floyd@flyhighassociates.eu, signupTimestamp=1673236064045]
FROM CASSANDRA => latest (5/19) emails: carson.maddox@interdemconsulting.biz, nolan.floyd@flyhighassociates.eu, aria.burke@interdemassociates.biz, layla.burks@memortech.com, joshua.chandler@furbainc.com...
202
----

.View Pulsar function logs by executing the following command
----
docker logs pulsar
----
.which should contain signup logs such as
----
Processing Signup(signupTier=ENTERPRISE, firstName=Gavin, lastName=Wilson, email=gavin.wilson@beans.eu, signupTimestamp=1673196872351)
    ENTERPRISE signup count: 1
Converting to Signup(signupTier=ENTERPRISE, firstName=Gavin, lastName=Wilson, email=gavin.wilson@beans.eu, signupTimestamp=1673196872351)
Processing Signup(signupTier=FREE, firstName=Nevaeh, lastName=Sexton, email=nevaeh.sexton@linger.eu, signupTimestamp=1673196877357)
    FREE signup count: 1
Processing Signup(signupTier=ENTERPRISE, firstName=Charlotte, lastName=Beach, email=charlotte.beach@quickerconsulting.eu, signupTimestamp=1673196882364)
    ENTERPRISE signup count: 2
Converting to Signup(signupTier=ENTERPRISE, firstName=Charlotte, lastName=Beach, email=charlotte.beach@quickerconsulting.eu, signupTimestamp=1673196882364)
----

==== Select from Cassandra
Each `ENTERPRISE` signup should result in a record in the Cassandra table.
To inspect all customer onboard records you can query the Cassandra table.

.Invoke the `CQLSH` utility on the cassandra container w/ the following command
----
docker exec -it cassandra cqlsh cassandra
----
.From the `cqlsh>` prompt execute the following
----
use sample_pulsar_functions_keyspace;
select * from customer_onboard;
exit;
----
.which should produce output similar to
----
 customer_email                       | customer_details
--------------------------------------+-----------------------------------------------------------------------------------------------------------------------------
               molly.mckay@morsem.com |                   {"firstName":"Molly","lastName":"Mckay","email":"molly.mckay@morsem.com","signupTimestamp":1673196862339}
                gavin.wilson@beans.eu |                   {"firstName":"Gavin","lastName":"Wilson","email":"gavin.wilson@beans.eu","signupTimestamp":1673196872351}
               ryan.ramsey@felics.biz |                   {"firstName":"Ryan","lastName":"Ramsey","email":"ryan.ramsey@felics.biz","signupTimestamp":1673196892373}
----

=== Stop app and services
Stop the sample app by entering `CTRL-C` in terminal it is running in.

Stop all running services using Docker Compose with the following command:
----
docker-compose down -v
----

== Useful commands

.Details about source
    docker exec -ti pulsar bin/pulsar-admin sources get --name UserSignupRabbitSource

.Details about sink
    docker exec -ti pulsar bin/pulsar-admin sinks get --name CustomerOnboardCassandraSink

.Details about function
    docker exec -ti pulsar bin/pulsar-admin functions get --name UserSignupFunction

.Consume messages from output topic of Signup function
    docker exec -ti pulsar bin/pulsar-client consume customer_onboard -s "co-sub1" -p "Earliest" -n 100