From ab5ece8ac55f260303a59d59694de59a81853324 Mon Sep 17 00:00:00 2001 From: Oliver Howell Date: Mon, 16 Sep 2024 12:08:37 +0100 Subject: [PATCH 01/16] Feast main fixes + API links (#1291) Forwardports changes implemented for 5.5 at release --------- Co-authored-by: rebekah-lawrence <142301480+rebekah-lawrence@users.noreply.github.com> --- docs/modules/ROOT/nav.adoc | 18 +++++++++++++++--- docs/modules/clients/pages/cplusplus.adoc | 2 ++ docs/modules/clients/pages/dotnet.adoc | 2 ++ docs/modules/clients/pages/go.adoc | 2 ++ docs/modules/clients/pages/java.adoc | 2 ++ docs/modules/clients/pages/memcache.adoc | 7 +++---- docs/modules/clients/pages/nodejs.adoc | 2 ++ docs/modules/clients/pages/python.adoc | 2 ++ docs/modules/integrate/pages/feast-config.adoc | 1 - .../pages/feature-engineering-with-feast.adoc | 2 +- .../integrate/pages/install-connect.adoc | 1 - .../integrate/pages/integrate-with-feast.adoc | 5 ++--- .../pages/streaming-features-with-feast.adoc | 3 ++- 13 files changed, 35 insertions(+), 14 deletions(-) diff --git a/docs/modules/ROOT/nav.adoc b/docs/modules/ROOT/nav.adoc index 0608e4ecd..d0633f6ba 100644 --- a/docs/modules/ROOT/nav.adoc +++ b/docs/modules/ROOT/nav.adoc @@ -166,7 +166,11 @@ include::wan:partial$nav.adoc[] .Integrate * xref:integrate:connectors.adoc[Overview] -* Integrating with Spring +* Files +// Files need an overview (options, what's available for SQL, what's available for Jet API) +** xref:integrate:file-connector.adoc[] +** xref:integrate:legacy-file-connector.adoc[] +* Integrate with Spring ** xref:spring:overview.adoc[Overview] ** xref:spring:configuration.adoc[] ** xref:spring:springaware.adoc[] @@ -175,9 +179,17 @@ include::wan:partial$nav.adoc[] ** xref:spring:transaction-manager.adoc[] ** xref:spring:best-practices.adoc[] * xref:integrate:integrate-with-feast.adoc[] -** xref:integrate:install-connect.adoc[] +** xref:integrate:install-connect.adoc[Install and connect Feast] ** xref:integrate:feast-config.adoc[] -** xref:integrate:feature-engineering-with-feast.adoc[] +** xref:integrate:feature-engineering-with-feast.adoc[Get started with Feast batch features] +** xref:integrate:streaming-features-with-feast.adoc[Get started with Feast streaming features] +* xref:integrate:kafka-connect-connectors.adoc[] +* Messaging System Connectors +** xref:integrate:messaging-system-connectors.adoc[Overview] +** xref:integrate:kafka-connector.adoc[] +** xref:integrate:kinesis-connector.adoc[] +** xref:integrate:jms-connector.adoc[] +** xref:integrate:pulsar-connector.adoc[] * Data Structure Connectors // Need an overview ** xref:integrate:map-connector.adoc[] diff --git a/docs/modules/clients/pages/cplusplus.adoc b/docs/modules/clients/pages/cplusplus.adoc index f43456413..4d341ba9e 100644 --- a/docs/modules/clients/pages/cplusplus.adoc +++ b/docs/modules/clients/pages/cplusplus.adoc @@ -2,6 +2,8 @@ :page-api-reference: http://hazelcast.github.io/hazelcast-cpp-client/{page-latest-supported-cplusplus-client}/index.html [[c-client]] +TIP: For the latest C{plus}{plus} API documentation, see http://hazelcast.github.io/hazelcast-cpp-client/{page-latest-supported-cplusplus-client}/index.html[Hazelcast C++ Client docs]. + You can use the native {cpp} client to connect to Hazelcast cluster members and perform almost all operations that a member can perform. Clients differ from members in that clients do not hold data. The {cpp} client is diff --git a/docs/modules/clients/pages/dotnet.adoc b/docs/modules/clients/pages/dotnet.adoc index e1e66424a..ab2159fde 100644 --- a/docs/modules/clients/pages/dotnet.adoc +++ b/docs/modules/clients/pages/dotnet.adoc @@ -2,6 +2,8 @@ :page-api-reference: http://hazelcast.github.io/hazelcast-csharp-client/{page-latest-supported-csharp-client}/api/index.html [[net-client]] +TIP: For the latest .NET API documentation, see http://hazelcast.github.io/hazelcast-csharp-client/{page-latest-supported-csharp-client}/api/index.html[Hazelcast .NET Client docs]. + You can use the native .NET client to connect to Hazelcast client members. The API is very similar to the Java native client. diff --git a/docs/modules/clients/pages/go.adoc b/docs/modules/clients/pages/go.adoc index 8a3fd2f0a..9d36ca59a 100644 --- a/docs/modules/clients/pages/go.adoc +++ b/docs/modules/clients/pages/go.adoc @@ -1,6 +1,8 @@ = Go Client :page-api-reference: https://pkg.go.dev/github.com/hazelcast/hazelcast-go-client@v{page-latest-supported-go-client} +TIP: For the latest Go API documentation, see https://pkg.go.dev/github.com/hazelcast/hazelcast-go-client@v{page-latest-supported-go-client}[Hazelcast Go Client docs]. + Go Client implementation for Hazelcast. It is implemented using the Hazelcast Open Binary Client Protocol. See Hazelcast Go client's GitHub https://github.com/hazelcast/hazelcast-go-client[repo^] diff --git a/docs/modules/clients/pages/java.adoc b/docs/modules/clients/pages/java.adoc index fb77124e1..1d2d5fa54 100644 --- a/docs/modules/clients/pages/java.adoc +++ b/docs/modules/clients/pages/java.adoc @@ -4,6 +4,8 @@ :page-toclevels: 3 [[java-client]] +TIP: For the latest Java API documentation, see https://docs.hazelcast.org/docs/{page-latest-supported-java-client}/javadoc[Hazelcast Java Client docs]. + To get started, include the `hazelcast.jar` dependency in your classpath. Once included, you can start using this client as if you are using the Hazelcast API. The differences are discussed in the below sections. diff --git a/docs/modules/clients/pages/memcache.adoc b/docs/modules/clients/pages/memcache.adoc index b46c460e3..974afc0a8 100644 --- a/docs/modules/clients/pages/memcache.adoc +++ b/docs/modules/clients/pages/memcache.adoc @@ -1,12 +1,11 @@ = Memcache Client -NOTE: Hazelcast Memcache Client only supports ASCII protocol. Binary Protocol is not supported. - A Memcache client written in any language can talk directly to a Hazelcast cluster. No additional configuration is required. -To be able to use a Memcache client, you must enable -the Memcache client request listener service using either one of the following configuration options: +NOTE: Hazelcast Memcache Client only supports ASCII protocol. Binary Protocol is not supported. + +To be able to use a Memcache client, you must enable the Memcache client request listener service using either one of the following configuration options: 1 - Using the `network` configuration element: diff --git a/docs/modules/clients/pages/nodejs.adoc b/docs/modules/clients/pages/nodejs.adoc index e844e9d41..e90c3fd82 100644 --- a/docs/modules/clients/pages/nodejs.adoc +++ b/docs/modules/clients/pages/nodejs.adoc @@ -1,6 +1,8 @@ = Node.js Client :page-api-reference: http://hazelcast.github.io/hazelcast-nodejs-client/api/{page-latest-supported-nodejs-client}/docs/ +TIP: For the latest Node.js API documentation, see http://hazelcast.github.io/hazelcast-nodejs-client/api/{page-latest-supported-nodejs-client}/docs/[Hazelcast Node.js Client docs]. + Node.js Client implementation for Hazelcast. It is implemented using the Hazelcast Open Binary Client Protocol. See Hazelcast Node.js client's GitHub https://github.com/hazelcast/hazelcast-nodejs-client[repo^] diff --git a/docs/modules/clients/pages/python.adoc b/docs/modules/clients/pages/python.adoc index 83b5e9b63..00541ca77 100644 --- a/docs/modules/clients/pages/python.adoc +++ b/docs/modules/clients/pages/python.adoc @@ -1,6 +1,8 @@ = Python Client :page-api-reference: https://hazelcast.readthedocs.io/en/v{page-latest-supported-python-client}/index.html +TIP: For the latest Python API documentation, see https://hazelcast.readthedocs.io/en/v{page-latest-supported-python-client}/index.html[Hazelcast Python Client docs]. + Python Client implementation for Hazelcast. It is implemented using the Hazelcast Open Binary Client Protocol. See Hazelcast Python client's GitHub https://github.com/hazelcast/hazelcast-python-client[repo^] diff --git a/docs/modules/integrate/pages/feast-config.adoc b/docs/modules/integrate/pages/feast-config.adoc index 79d9ae5db..11f34092b 100644 --- a/docs/modules/integrate/pages/feast-config.adoc +++ b/docs/modules/integrate/pages/feast-config.adoc @@ -1,5 +1,4 @@ = Configure Feature Store -:page-enterprise: true :description: To use a Feast project, you must configure the feature store. The configuration is defined in a YAML configuration file. {description} diff --git a/docs/modules/integrate/pages/feature-engineering-with-feast.adoc b/docs/modules/integrate/pages/feature-engineering-with-feast.adoc index 9cc3e5cc1..8fd7a8c79 100644 --- a/docs/modules/integrate/pages/feature-engineering-with-feast.adoc +++ b/docs/modules/integrate/pages/feature-engineering-with-feast.adoc @@ -13,7 +13,7 @@ Finally, you will transfer the features in the offline store to the online store You will need the following ready before starting the tutorial: -* Hazelcast CLC. link:https://docs.hazelcast.com/clc/latest/install-clc[Installation instructions] +* Hazelcast CLC (see link:https://docs.hazelcast.com/clc/latest/install-clc[Install CLC]) * A recent version of Docker and Docker Compose To set up your project, complete the following steps: diff --git a/docs/modules/integrate/pages/install-connect.adoc b/docs/modules/integrate/pages/install-connect.adoc index 498483c2e..d136f097a 100644 --- a/docs/modules/integrate/pages/install-connect.adoc +++ b/docs/modules/integrate/pages/install-connect.adoc @@ -1,5 +1,4 @@ = Install Feast and Connect to Hazelcast -:page-enterprise: true :description: Before you can use Feast with Hazelcast as an online store, you must install Feast and connect to Hazelcast. {description} diff --git a/docs/modules/integrate/pages/integrate-with-feast.adoc b/docs/modules/integrate/pages/integrate-with-feast.adoc index 4c5464af7..79c2c8d38 100644 --- a/docs/modules/integrate/pages/integrate-with-feast.adoc +++ b/docs/modules/integrate/pages/integrate-with-feast.adoc @@ -1,5 +1,4 @@ = Integrate with Feast -:page-enterprise: true :description: pass:q[Feast (**Fea**ture **St**ore) is a customizable operational data system, which uses your existing infratstructure to manage and serve machine learning features to real-time models. When integrated with Hazelcast, you can benefit from an online store that supports materializing feature values in a running Hazelcast cluster.] {description} @@ -115,5 +114,5 @@ To use Feast with Hazelcast, you must do the following: You can also work through the following tutorials: -* Get Started with Feature Store -* Feature Compute and Transformation +* xref:integrate:feature-engineering-with-feast.adoc[Get started with Feast streaming] +* xref:integrate:streaming-features-with-feast.adoc[Get started with Feast feature engineering] diff --git a/docs/modules/integrate/pages/streaming-features-with-feast.adoc b/docs/modules/integrate/pages/streaming-features-with-feast.adoc index 74d5119db..62e502b2b 100644 --- a/docs/modules/integrate/pages/streaming-features-with-feast.adoc +++ b/docs/modules/integrate/pages/streaming-features-with-feast.adoc @@ -12,7 +12,7 @@ Then update the online feature store using a Jet job in real time from transacti You will need the following ready before starting the tutorial: -* Hazelcast CLC - link:https://docs.hazelcast.com/clc/latest/install-clc[Installation instructions] +* Hazelcast CLC (see link:https://docs.hazelcast.com/clc/latest/install-clc[Install CLC]) * A recent version of Docker and Docker Compose To set up your project, complete the following steps: @@ -346,6 +346,7 @@ Outputs something similar to: ] } ---- + == Summary In this tutorial, you learned how to set up a feature engineering project that uses Hazelcast as the online store. From d37b8a8ae03b8a0dd67cbcab11d267307c49ca4e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Y=C3=BCce=20Tekol?= Date: Mon, 16 Sep 2024 15:30:52 +0300 Subject: [PATCH 02/16] Feast diagrams (#1288) --- docs/modules/ROOT/images/feast_batch.png | Bin 0 -> 52075 bytes docs/modules/ROOT/images/feast_streaming.png | Bin 0 -> 47068 bytes .../pages/feature-engineering-with-feast.adoc | 5 ++++- .../pages/streaming-features-with-feast.adoc | 5 ++++- 4 files changed, 8 insertions(+), 2 deletions(-) create mode 100644 docs/modules/ROOT/images/feast_batch.png create mode 100644 docs/modules/ROOT/images/feast_streaming.png diff --git a/docs/modules/ROOT/images/feast_batch.png b/docs/modules/ROOT/images/feast_batch.png new file mode 100644 index 0000000000000000000000000000000000000000..8c0f458a2fefee997c5496f6e52bc8dd5d12f326 GIT binary patch literal 52075 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 literal 0 HcmV?d00001 diff --git a/docs/modules/integrate/pages/feature-engineering-with-feast.adoc b/docs/modules/integrate/pages/feature-engineering-with-feast.adoc index 8fd7a8c79..11681984a 100644 --- a/docs/modules/integrate/pages/feature-engineering-with-feast.adoc +++ b/docs/modules/integrate/pages/feature-engineering-with-feast.adoc @@ -1,4 +1,4 @@ -= Feature Engineering with Feast += Get Started with Feast Batch Features :description: This tutorial will get you started with feature engineering using the Hazelcast integration with Feast. {description} @@ -9,6 +9,9 @@ You will setup an offline feature store with PostgreSQL and an online feature st Then, populate the offline feature store using Jet jobs. Finally, you will transfer the features in the offline store to the online store. +.Overview of Feast batch workflow +image:ROOT:feast_batch.png[Feast batch wokflow] + == Before you Begin You will need the following ready before starting the tutorial: diff --git a/docs/modules/integrate/pages/streaming-features-with-feast.adoc b/docs/modules/integrate/pages/streaming-features-with-feast.adoc index 62e502b2b..b11d5b6cc 100644 --- a/docs/modules/integrate/pages/streaming-features-with-feast.adoc +++ b/docs/modules/integrate/pages/streaming-features-with-feast.adoc @@ -1,4 +1,4 @@ -= Feast Streaming Features += Get Started with Feast Streaming Features :description: This tutorial will get you started with streaming features using the Hazelcast integration with Feast. {description} @@ -8,6 +8,9 @@ You will setup an offline feature store with PostgreSQL and an online feature store with Hazelcast. Then update the online feature store using a Jet job in real time from transactions streaming in a Kafka topic. +.Overview of Feast streaming workflow +image:ROOT:feast_streaming.png[Feast streaming wokflow] + == Before you Begin You will need the following ready before starting the tutorial: From 9d71d74b1ef7d943ca571d1ef4a4d9941689ca54 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Y=C3=BCce=20Tekol?= Date: Mon, 16 Sep 2024 16:01:59 +0300 Subject: [PATCH 03/16] Fix Feast doc section header (#1292) --- docs/modules/integrate/pages/streaming-features-with-feast.adoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/modules/integrate/pages/streaming-features-with-feast.adoc b/docs/modules/integrate/pages/streaming-features-with-feast.adoc index b11d5b6cc..a6fa4f333 100644 --- a/docs/modules/integrate/pages/streaming-features-with-feast.adoc +++ b/docs/modules/integrate/pages/streaming-features-with-feast.adoc @@ -139,7 +139,7 @@ Note that the command below prevents the `feast` process from outputting to the ---- feast -c feature_repo serve -h 0.0.0.0 -p 6566 --no-access-log 2> /dev/null & ---- -+ + == Feature Transformation Using Jet From c3bee3f4ad041e77a48720d1afb63078980da4f1 Mon Sep 17 00:00:00 2001 From: Oliver Howell Date: Tue, 17 Sep 2024 11:09:02 +0100 Subject: [PATCH 04/16] Fix nav duplication from cherrypicking (#1294) --- docs/modules/ROOT/nav.adoc | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/docs/modules/ROOT/nav.adoc b/docs/modules/ROOT/nav.adoc index d0633f6ba..a75dbd4e9 100644 --- a/docs/modules/ROOT/nav.adoc +++ b/docs/modules/ROOT/nav.adoc @@ -184,12 +184,6 @@ include::wan:partial$nav.adoc[] ** xref:integrate:feature-engineering-with-feast.adoc[Get started with Feast batch features] ** xref:integrate:streaming-features-with-feast.adoc[Get started with Feast streaming features] * xref:integrate:kafka-connect-connectors.adoc[] -* Messaging System Connectors -** xref:integrate:messaging-system-connectors.adoc[Overview] -** xref:integrate:kafka-connector.adoc[] -** xref:integrate:kinesis-connector.adoc[] -** xref:integrate:jms-connector.adoc[] -** xref:integrate:pulsar-connector.adoc[] * Data Structure Connectors // Need an overview ** xref:integrate:map-connector.adoc[] @@ -202,10 +196,6 @@ include::wan:partial$nav.adoc[] ** xref:integrate:cdc-connectors.adoc[] ** xref:integrate:elasticsearch-connector.adoc[] ** xref:integrate:mongodb-connector.adoc[] -* File Connectors -// Files need an overview -** xref:integrate:file-connector.adoc[] -** xref:integrate:legacy-file-connector.adoc[] * xref:integrate:messaging-system-connectors.adoc[Messaging System Connectors] ** xref:integrate:kafka-connector.adoc[] ** xref:integrate:kinesis-connector.adoc[] From 767458fd5d0c72a746835ee7aca3f97a730c47ac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Franti=C5=A1ek=20Hartman?= Date: Tue, 17 Sep 2024 13:48:22 +0200 Subject: [PATCH 05/16] Document missing property type-name for GenericMapStore/Loader [SUP-479] (#1185) Fixes #1184 --- .../configuring-a-generic-maploader.adoc | 66 +++++++++++++++++-- .../pages/configuring-a-generic-mapstore.adoc | 63 +++++++++++++++++- 2 files changed, 122 insertions(+), 7 deletions(-) diff --git a/docs/modules/mapstore/pages/configuring-a-generic-maploader.adoc b/docs/modules/mapstore/pages/configuring-a-generic-maploader.adoc index 29f7d15ff..c1abd38aa 100644 --- a/docs/modules/mapstore/pages/configuring-a-generic-maploader.adoc +++ b/docs/modules/mapstore/pages/configuring-a-generic-maploader.adoc @@ -373,6 +373,65 @@ mapConfig.setMapStoreConfig(mapStoreConfig); -- ==== +|[[columns]]`type-name` +|The type name of the compact GenericRecord. Use this property to map your record to an existing domain class. + +| +The name of the map. +| + +[tabs] +==== +XML:: ++ +-- +[source,xml] +---- + + + + com.hazelcast.mapstore.GenericMapStore + + my-mysql-database + org.example.Person + + + +---- +-- +YAML:: ++ +-- +[source,yaml] +---- +hazelcast: + map: + mymapname: + map-store: + enabled: true + class-name: com.hazelcast.mapstore.GenericMapStore + properties: + data-connection-ref: my-mysql-database + type-name: org.example.Person +---- +-- +Java:: ++ +-- +[source,java] +---- +MapConfig mapConfig = new MapConfig("myMapName"); + +MapStoreConfig mapStoreConfig = new MapStoreConfig(); +mapStoreConfig.setClassName("com.hazelcast.mapstore.GenericMapStore"); +mapStoreConfig.setProperty("data-connection-ref", "my-mysql-database"); +mapStoreConfig.setProperty("type-name", "org.example.Person"); + +mapConfig.setMapStoreConfig(mapStoreConfig); +---- +-- +==== + |=== == Supported backends @@ -381,11 +440,8 @@ GenericMapStore needs a SQL Connector that supports `SELECT`, `UPDATE`, `SINK IN Officially supported connectors: -- JDBC Connector - * supports MySQL, PostgreSQL. - * requires JDBC driver on the classpath -- MongoDB Connector - * make sure you have `hazelcast-jet-mongodb` artifact included on the classpath. +- MySQL, PostgreSQL, Microsoft SQL Server, Oracle (it uses JDBC SQL Connector). +- MongoDB (make sure you have `hazelcast-jet-mongodb` artifact included on the classpath). == Related Resources diff --git a/docs/modules/mapstore/pages/configuring-a-generic-mapstore.adoc b/docs/modules/mapstore/pages/configuring-a-generic-mapstore.adoc index e36b33f78..f24407057 100644 --- a/docs/modules/mapstore/pages/configuring-a-generic-mapstore.adoc +++ b/docs/modules/mapstore/pages/configuring-a-generic-mapstore.adoc @@ -373,13 +373,72 @@ mapConfig.setMapStoreConfig(mapStoreConfig); -- ==== +|[[columns]]`type-name` +|The type name of the compact GenericRecord. Use this property to map your record to an existing domain class. + +| +The name of the map. +| + +[tabs] +==== +XML:: ++ +-- +[source,xml] +---- + + + + com.hazelcast.mapstore.GenericMapStore + + my-mysql-database + org.example.Person + + + +---- +-- +YAML:: ++ +-- +[source,yaml] +---- +hazelcast: + map: + mymapname: + map-store: + enabled: true + class-name: com.hazelcast.mapstore.GenericMapStore + properties: + data-connection-ref: my-mysql-database + type-name: org.example.Person +---- +-- +Java:: ++ +-- +[source,java] +---- +MapConfig mapConfig = new MapConfig("myMapName"); + +MapStoreConfig mapStoreConfig = new MapStoreConfig(); +mapStoreConfig.setClassName("com.hazelcast.mapstore.GenericMapStore"); +mapStoreConfig.setProperty("data-connection-ref", "my-mysql-database"); +mapStoreConfig.setProperty("type-name", "org.example.Person"); + +mapConfig.setMapStoreConfig(mapStoreConfig); +---- +-- +==== + |=== == Supported backends -You can use any database as the MapStore backend as long as you have its Hazelcast SQL Connector on the classpath. +GenericMapStore needs a SQL Connector that supports `SELECT`, `UPDATE`, `SINK INTO` and `DELETE` statements. -Officially supported backend databases: +Officially supported connectors: - MySQL, PostgreSQL, Microsoft SQL Server, Oracle (it uses JDBC SQL Connector). - MongoDB (make sure you have `hazelcast-jet-mongodb` artifact included on the classpath). From 886a642fd007ac80955df18b375e4395fe3d5017 Mon Sep 17 00:00:00 2001 From: Oliver Howell Date: Wed, 18 Sep 2024 08:56:37 +0100 Subject: [PATCH 06/16] Update nav for Develop and Build changes (#1295) Covers nav changes suggested in https://github.com/hazelcast/hz-docs/pull/1293 Note: Backport label might fail / be messy - needs checked and might have to manually backport by updating nav file Co-authored-by: Avtar <118772076+avtarraikmo@users.noreply.github.com> --- docs/modules/ROOT/nav.adoc | 51 ++++++++++++++++++++++---------------- 1 file changed, 30 insertions(+), 21 deletions(-) diff --git a/docs/modules/ROOT/nav.adoc b/docs/modules/ROOT/nav.adoc index a75dbd4e9..758fba3e1 100644 --- a/docs/modules/ROOT/nav.adoc +++ b/docs/modules/ROOT/nav.adoc @@ -45,10 +45,7 @@ .Develop & build -* Ingestion -** xref:ingest:overview.adoc[] -** xref:computing:distributed-computing.adoc[] -** xref:query:overview.adoc[] +include::architecture:partial$nav.adoc[] * xref:cluster-performance:best-practices.adoc[] ** xref:capacity-planning.adoc[] ** xref:cluster-performance:performance-tips.adoc[] @@ -59,7 +56,6 @@ ** xref:cluster-performance:near-cache.adoc[] ** xref:cluster-performance:imap-bulk-read-operations.adoc[] ** xref:cluster-performance:data-affinity.adoc[] -include::architecture:partial$nav.adoc[] * Member/Client Discovery ** xref:clusters:discovery-mechanisms.adoc[] ** xref:clusters:discovering-by-tcp.adoc[] @@ -67,11 +63,20 @@ include::architecture:partial$nav.adoc[] ** xref:clusters:discovering-native-clients.adoc[] include::kubernetes:partial$nav.adoc[] include::data-structures:partial$nav.adoc[] -* Distributed Compute +// Distributed Computing +* xref:computing:distributed-computing.adoc[] +// ** xref:data-structures:entry-processor.adoc[] also dupe, commenting for now ** Executor Services include::computing:partial$nav.adoc[] +// UCN ** xref:clusters:user-code-namespaces.adoc[] include::clusters:partial$nav.adoc[] +// Pipelines +// ** xref:pipelines:overview.adoc[Data Pipelines] this duped in partial nav below +// Ingestion +* Data Ingestion & Query +** xref:ingest:overview.adoc[] +** xref:query:overview.adoc[] include::mapstore:partial$nav.adoc[] include::pipelines:partial$nav.adoc[] * SQL @@ -166,10 +171,6 @@ include::wan:partial$nav.adoc[] .Integrate * xref:integrate:connectors.adoc[Overview] -* Files -// Files need an overview (options, what's available for SQL, what's available for Jet API) -** xref:integrate:file-connector.adoc[] -** xref:integrate:legacy-file-connector.adoc[] * Integrate with Spring ** xref:spring:overview.adoc[Overview] ** xref:spring:configuration.adoc[] @@ -183,26 +184,34 @@ include::wan:partial$nav.adoc[] ** xref:integrate:feast-config.adoc[] ** xref:integrate:feature-engineering-with-feast.adoc[Get started with Feast batch features] ** xref:integrate:streaming-features-with-feast.adoc[Get started with Feast streaming features] -* xref:integrate:kafka-connect-connectors.adoc[] -* Data Structure Connectors +// Connectors +* Messaging System Connectors +* xref:integrate:messaging-system-connectors.adoc[Overview] +** xref:integrate:kafka-connector.adoc[] +** xref:integrate:kinesis-connector.adoc[] +** xref:integrate:jms-connector.adoc[] +// Database and CDC +* xref:integrate:database-connectors.adoc[Database & CDC Connectors] +** xref:integrate:jdbc-connector.adoc[] +** xref:integrate:cdc-connectors.adoc[] +** xref:integrate:elasticsearch-connector.adoc[] +** xref:integrate:mongodb-connector.adoc[] +* File Connectors +// Need an overview +** xref:integrate:file-connector.adoc[] +** xref:integrate:legacy-file-connector.adoc[] +* Hazelcast Data Structure Connectors // Need an overview ** xref:integrate:map-connector.adoc[] ** xref:integrate:jcache-connector.adoc[] ** xref:integrate:list-connector.adoc[] ** xref:integrate:reliable-topic-connector.adoc[] ** xref:integrate:vector-collection-connector.adoc[] -* xref:integrate:database-connectors.adoc[Database Connectors] -** xref:integrate:jdbc-connector.adoc[] -** xref:integrate:cdc-connectors.adoc[] -** xref:integrate:elasticsearch-connector.adoc[] -** xref:integrate:mongodb-connector.adoc[] -* xref:integrate:messaging-system-connectors.adoc[Messaging System Connectors] -** xref:integrate:kafka-connector.adoc[] -** xref:integrate:kinesis-connector.adoc[] -** xref:integrate:jms-connector.adoc[] +// Kafka Connect * xref:integrate:kafka-connect-connectors.adoc[] * xref:integrate:socket-connector.adoc[] * xref:integrate:test-connectors.adoc[] +// Custom & Community * xref:integrate:custom-connectors.adoc[] * Community Connectors // Need an overview From 64f9bda3fb0e972df49b72f819acb1275a7da08e Mon Sep 17 00:00:00 2001 From: Jack Green Date: Mon, 30 Sep 2024 10:14:18 +0100 Subject: [PATCH 07/16] Use latest `checkout` action (#1206) Rather than hardcoding a specific (now outdated) version of `checkout`, we can specify the latest `4.x` as per everywhere else. --- .github/workflows/action-updater.yml | 2 +- .github/workflows/adoc-html.yml | 2 +- .github/workflows/backport-5-0.yml | 2 +- .github/workflows/backport-5-1.yml | 2 +- .github/workflows/backport-5-2.yml | 2 +- .github/workflows/backport-5-3.yml | 2 +- .github/workflows/backport-5-4.yml | 2 +- .github/workflows/backport.yml | 2 +- .github/workflows/forwardport.yml | 2 +- .github/workflows/to-plain-html.yml | 2 +- .github/workflows/validate.yml | 2 +- 11 files changed, 11 insertions(+), 11 deletions(-) diff --git a/.github/workflows/action-updater.yml b/.github/workflows/action-updater.yml index ca2c6ba9d..e93f79166 100644 --- a/.github/workflows/action-updater.yml +++ b/.github/workflows/action-updater.yml @@ -13,7 +13,7 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4.1.4 + - uses: actions/checkout@v4 with: # [Required] Access token with `workflow` scope. token: ${{ secrets.ACTION_UPDATER }} diff --git a/.github/workflows/adoc-html.yml b/.github/workflows/adoc-html.yml index 72c4c80ec..e68c81cef 100644 --- a/.github/workflows/adoc-html.yml +++ b/.github/workflows/adoc-html.yml @@ -9,7 +9,7 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4.1.4 + - uses: actions/checkout@v4 - uses: actions/setup-node@v4.0.2 with: node-version: 20 diff --git a/.github/workflows/backport-5-0.yml b/.github/workflows/backport-5-0.yml index a895ad53e..3dffc7647 100644 --- a/.github/workflows/backport-5-0.yml +++ b/.github/workflows/backport-5-0.yml @@ -12,7 +12,7 @@ jobs: steps: - name: checkout - uses: actions/checkout@v4.1.4 + uses: actions/checkout@v4 with: fetch-depth: 0 diff --git a/.github/workflows/backport-5-1.yml b/.github/workflows/backport-5-1.yml index 42515b977..5964dfc32 100644 --- a/.github/workflows/backport-5-1.yml +++ b/.github/workflows/backport-5-1.yml @@ -12,7 +12,7 @@ jobs: steps: - name: checkout - uses: actions/checkout@v4.1.4 + uses: actions/checkout@v4 with: fetch-depth: 0 diff --git a/.github/workflows/backport-5-2.yml b/.github/workflows/backport-5-2.yml index cb57cada4..47f5aed75 100644 --- a/.github/workflows/backport-5-2.yml +++ b/.github/workflows/backport-5-2.yml @@ -12,7 +12,7 @@ jobs: steps: - name: checkout - uses: actions/checkout@v4.1.4 + uses: actions/checkout@v4 with: fetch-depth: 0 diff --git a/.github/workflows/backport-5-3.yml b/.github/workflows/backport-5-3.yml index 3a2db103e..8c281af0a 100644 --- a/.github/workflows/backport-5-3.yml +++ b/.github/workflows/backport-5-3.yml @@ -12,7 +12,7 @@ jobs: steps: - name: checkout - uses: actions/checkout@v4.1.4 + uses: actions/checkout@v4 with: fetch-depth: 0 diff --git a/.github/workflows/backport-5-4.yml b/.github/workflows/backport-5-4.yml index fdd62ebf9..c6ff5061a 100644 --- a/.github/workflows/backport-5-4.yml +++ b/.github/workflows/backport-5-4.yml @@ -12,7 +12,7 @@ jobs: steps: - name: checkout - uses: actions/checkout@v4.1.4 + uses: actions/checkout@v4 with: fetch-depth: 0 diff --git a/.github/workflows/backport.yml b/.github/workflows/backport.yml index a5ba1108c..2ad888348 100644 --- a/.github/workflows/backport.yml +++ b/.github/workflows/backport.yml @@ -12,7 +12,7 @@ jobs: steps: - name: checkout - uses: actions/checkout@v4.1.4 + uses: actions/checkout@v4 with: fetch-depth: 0 diff --git a/.github/workflows/forwardport.yml b/.github/workflows/forwardport.yml index d1e591d00..48f007ddc 100644 --- a/.github/workflows/forwardport.yml +++ b/.github/workflows/forwardport.yml @@ -12,7 +12,7 @@ jobs: steps: - name: checkout - uses: actions/checkout@v4.1.4 + uses: actions/checkout@v4 with: fetch-depth: 0 diff --git a/.github/workflows/to-plain-html.yml b/.github/workflows/to-plain-html.yml index b5ba50676..2320157b9 100644 --- a/.github/workflows/to-plain-html.yml +++ b/.github/workflows/to-plain-html.yml @@ -8,7 +8,7 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4.1.4 + - uses: actions/checkout@v4 with: token: ${{ secrets.TO_HTML }} - name: Asciidoc to html diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml index 5c1f2a4c5..3ca1ce81d 100644 --- a/.github/workflows/validate.yml +++ b/.github/workflows/validate.yml @@ -13,7 +13,7 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4.1.4 + - uses: actions/checkout@v4 - uses: actions/setup-node@v4.0.2 with: node-version: 20 From ea38a79a0293bab79801d9663efd25cad6332334 Mon Sep 17 00:00:00 2001 From: Oliver Howell Date: Mon, 30 Sep 2024 11:32:04 +0100 Subject: [PATCH 08/16] DOCS-903: add dependencies and POJO tip; backport type-name (#1297) Closes SUP-455 and DOCS-903 --- .../configuring-a-generic-maploader.adoc | 41 ++++++++++++++----- .../pages/configuring-a-generic-mapstore.adoc | 39 ++++++++++++++---- 2 files changed, 61 insertions(+), 19 deletions(-) diff --git a/docs/modules/mapstore/pages/configuring-a-generic-maploader.adoc b/docs/modules/mapstore/pages/configuring-a-generic-maploader.adoc index c1abd38aa..4752a4731 100644 --- a/docs/modules/mapstore/pages/configuring-a-generic-maploader.adoc +++ b/docs/modules/mapstore/pages/configuring-a-generic-maploader.adoc @@ -1,16 +1,37 @@ -= Using the Generic MapLoader += Using the generic MapLoader :description: With the xref:working-with-external-data.adoc#options[generic MapLoader], you can configure a map to cache data from an external system. This topic includes an example of how to configure a map with a generic MapLoader that connects to a MySQL database. :page-beta: false {description} +NOTE: The objects created in the distributed map are stored as GenericRecord. You can use the `type-name` property to store the data in a POJO (Plain Old Java Object). + For a list of all supported external systems, including databases, see available xref:external-data-stores:external-data-stores.adoc#connectors[data connection types]. -== Before you Begin +== Before you begin You need a xref:external-data-stores:external-data-stores.adoc[data connection] that's configured on all cluster members. -== Quickstart Configuration +== Add dependencies + +If you are using a Hazelcast JAR file, you need to ensure the following is added to your classpath: + +[source,xml] +---- + + com.hazelcast + hazelcast-sql + + + + com.hazelcast + hazelcast-mapstore + +---- + +NOTE: If you are using the slim distribution, you need to add `hazelcast-mapstore`. If you are using MongoDb, you also need to add `hazelcast-jet-mongodb`. + +== Quickstart configuration This example shows a basic map configuration that uses a data connection called `my-mysql-database`. See xref:data-structures:map.adoc[] for the details of other properties that you can include in your map configuration. @@ -70,11 +91,11 @@ instance().getConfig().addMapConfig(mapConfig); <2> The name of your data connection. [[mapping]] -== SQL Mapping for the Generic MapLoader +== SQL mapping for the generic MapLoader -When you configure a map with the generic MapLoader, Hazelcast creates a xref:sql:mapping-to-jdbc.adoc[SQL mapping with the JDBC connector]. The name of the mapping is the same name as your map prefixed with `__map-store.`. This mapping is used to read data from the external system, and it is removed whenever the configured map is removed. You can also configure this SQL mapping, using <>. +When you configure a map with the generic MapLoader, Hazelcast creates a xref:sql:mapping-to-jdbc.adoc[SQL mapping with the JDBC connector]. The name of the mapping is the same name as your map prefixed with `__map-store.`. This mapping is used to read data from the external system, and is removed whenever the configured map is removed. You can also configure this SQL mapping, using <>. -== Configuration Properties for the Generic MapLoader +== Configuration properties for the generic MapLoader These configuration properties allow you to configure the generic MapLoader and its SQL mapping. @@ -436,19 +457,19 @@ mapConfig.setMapStoreConfig(mapStoreConfig); == Supported backends -GenericMapStore needs a SQL Connector that supports `SELECT`, `UPDATE`, `SINK INTO` and `DELETE` statements. +The generic MapStore needs a SQL Connector that supports `SELECT`, `UPDATE`, `SINK INTO` and `DELETE` statements. Officially supported connectors: - MySQL, PostgreSQL, Microsoft SQL Server, Oracle (it uses JDBC SQL Connector). - MongoDB (make sure you have `hazelcast-jet-mongodb` artifact included on the classpath). -== Related Resources +== Related resources - To monitor MapStores for each loaded entry, use the `EntryLoadedListener` interface. See the xref:events:object-events.adoc#listening-for-map-events[Listening for Map Events section] to learn how you can catch entry-based events. - xref:mapstore-triggers.adoc[]. -== Next Steps +== Next steps -See the MapStore xref:configuration-guide.adoc[configuration guide] for details about configuration options, including caching behaviors. +See the xref:configuration-guide.adoc[MapStore configuration guide] for details about configuration options, including caching behaviors. diff --git a/docs/modules/mapstore/pages/configuring-a-generic-mapstore.adoc b/docs/modules/mapstore/pages/configuring-a-generic-mapstore.adoc index f24407057..77f146d94 100644 --- a/docs/modules/mapstore/pages/configuring-a-generic-mapstore.adoc +++ b/docs/modules/mapstore/pages/configuring-a-generic-mapstore.adoc @@ -1,16 +1,37 @@ -= Using the Generic MapStore += Using the generic MapStore :description: With the xref:working-with-external-data.adoc#options[generic MapStore], you can configure a map to cache data from and write data back to an external system. This topic includes an example of how to configure a map with a generic MapStore that connects to a MySQL database. :page-beta: false {description} +NOTE: The objects created in the distributed map are stored as GenericRecord. You can use the `type-name` property to store the data in a POJO (Plain Old Java Object). + For a list of all supported external systems, including databases, see available xref:external-data-stores:external-data-stores.adoc#connectors[data connection types]. -== Before you Begin +== Before you begin You need a xref:external-data-stores:external-data-stores.adoc[data connection] that's configured on all cluster members. -== Quickstart Configuration +== Add dependencies + +If you are using a Hazelcast JAR file, you need to ensure the following is added to your classpath: + +[source,xml] +---- + + com.hazelcast + hazelcast-sql + + + + com.hazelcast + hazelcast-mapstore + +---- + +NOTE: If you are using the slim distribution, you need to add `hazelcast-mapstore`. If you are using MongoDb, you also need to add `hazelcast-jet-mongodb`. + +== Quickstart configuration This example shows a basic map configuration that uses a data connection called `my-mysql-database`. See xref:data-structures:map.adoc[] for the details of other properties that you include in your map configuration. @@ -70,11 +91,11 @@ instance().getConfig().addMapConfig(mapConfig); <2> The name of your data connection. [[mapping]] -== SQL Mapping for the Generic MapStore +== SQL mapping for the generic MapStore -When you configure a map with the generic MapStore, Hazelcast creates a xref:sql:mapping-to-jdbc.adoc[SQL mapping with the JDBC connector]. The name of the mapping is the same name as your map prefixed with `__map-store.`. This mapping is used to read data from or write data to the external system and it is removed whenever the configured map is removed. You can also configure this SQL mapping, using <>. +When you configure a map with the generic MapStore, Hazelcast creates a xref:sql:mapping-to-jdbc.adoc[SQL mapping with the JDBC connector]. The name of the mapping is the same name as your map prefixed with `__map-store.`. This mapping is used to read data from or write data to the external system and is removed whenever the configured map is removed. You can also configure this SQL mapping, using <>. -== Configuration Properties for the Generic MapStore +== Configuration properties for the generic MapStore These configuration properties allow you to configure the generic MapStore and its SQL mapping. @@ -436,14 +457,14 @@ mapConfig.setMapStoreConfig(mapStoreConfig); == Supported backends -GenericMapStore needs a SQL Connector that supports `SELECT`, `UPDATE`, `SINK INTO` and `DELETE` statements. +The generic MapStore needs a SQL Connector that supports `SELECT`, `UPDATE`, `SINK INTO` and `DELETE` statements. Officially supported connectors: - MySQL, PostgreSQL, Microsoft SQL Server, Oracle (it uses JDBC SQL Connector). - MongoDB (make sure you have `hazelcast-jet-mongodb` artifact included on the classpath). -== Related Resources +== Related resources - To monitor MapStores for each loaded entry, use the `EntryLoadedListener` interface. See the xref:events:object-events.adoc#listening-for-map-events[Listening for Map Events section] to learn how you can catch entry-based events. @@ -451,4 +472,4 @@ Officially supported connectors: == Next Steps -See the MapStore xref:configuration-guide.adoc[configuration guide] for details about configuration options, including caching behaviors. +See the xref:configuration-guide.adoc[MapStore configuration guide] for details about configuration options, including caching behaviors. From 9901cec2d26c256f251879254d58392a64429106 Mon Sep 17 00:00:00 2001 From: James Holgate <130981049+JamesHazelcast@users.noreply.github.com> Date: Tue, 1 Oct 2024 10:46:45 +0100 Subject: [PATCH 09/16] Update UCN notices to mention incompatibility with Jet (#1275) Removes the upgrade tip from the CDC Join tutorial (which uses Jet), and updates the migration tip to mention that Jet should continue to use UCD (for now). This PR will be replaced in the future once HZG-88 is completed. --- docs/modules/clusters/partials/ucn-migrate-tip.adoc | 2 +- docs/modules/pipelines/pages/cdc-join.adoc | 2 -- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/docs/modules/clusters/partials/ucn-migrate-tip.adoc b/docs/modules/clusters/partials/ucn-migrate-tip.adoc index 88b7e6ec7..3b56e1023 100644 --- a/docs/modules/clusters/partials/ucn-migrate-tip.adoc +++ b/docs/modules/clusters/partials/ucn-migrate-tip.adoc @@ -1 +1 @@ -CAUTION: {ucd} has been deprecated and will be removed in the next major version. To continue deploying your user code after this time, {open-source-product-name} users can either upgrade to {enterprise-product-name}, or add their resources to the Hazelcast member class paths. Hazelcast recommends that {enterprise-product-name} users migrate their user code to use {ucn}. For further information on migrating from {ucd} to {ucn}, see the xref:clusters:ucn-migrate-ucd.adoc[] topic. \ No newline at end of file +CAUTION: {ucd} has been deprecated and will be removed in the next major version. To continue deploying your user code after this time, {open-source-product-name} users can either upgrade to {enterprise-product-name}, or add their resources to the Hazelcast member class paths. Hazelcast recommends that {enterprise-product-name} users migrate their user code to use {ucn} for all purposes other than Jet stream processing. For further information on migrating from {ucd} to {ucn}, see xref:clusters:ucn-migrate-ucd.adoc[]. \ No newline at end of file diff --git a/docs/modules/pipelines/pages/cdc-join.adoc b/docs/modules/pipelines/pages/cdc-join.adoc index 0ac6e59ab..448ad895f 100644 --- a/docs/modules/pipelines/pages/cdc-join.adoc +++ b/docs/modules/pipelines/pages/cdc-join.adoc @@ -514,8 +514,6 @@ You should see the following jars: . Enable user code deployment: + -include::clusters:partial$ucn-migrate-tip.adoc[] -+ Due to the type of sink we are using in our pipeline we need to make some extra changes in order for the Hazelcast cluster to be aware of the custom classes we have defined. + From 202d387f40252b0e23cdc8e5ba23622ed14c23d1 Mon Sep 17 00:00:00 2001 From: Oliver Howell Date: Wed, 2 Oct 2024 10:12:13 +0100 Subject: [PATCH 10/16] Delete docs/modules/cluster-performance/pages/bulk-read-operations.adoc Duplicate after rename --- .../pages/bulk-read-operations.adoc | 105 ------------------ 1 file changed, 105 deletions(-) delete mode 100644 docs/modules/cluster-performance/pages/bulk-read-operations.adoc diff --git a/docs/modules/cluster-performance/pages/bulk-read-operations.adoc b/docs/modules/cluster-performance/pages/bulk-read-operations.adoc deleted file mode 100644 index c941bfdf0..000000000 --- a/docs/modules/cluster-performance/pages/bulk-read-operations.adoc +++ /dev/null @@ -1,105 +0,0 @@ -= Bulk read operations -:description: Learn about best practices for IMap bulk read operations. - -[[bulk-read-operations]] - -To safeguard your cluster and application from becoming Out of Memory -(OOM), follow these best practices and consider using the described -alternatives to bulk read operations. - -It's critical to avoid an Out of Memory Error (OOME) as its impact -can be severe. Hazelcast strives to protect your data but -an OOME can lead to a loss of cluster availability. This can result -in increased operation latencies due to triggered migrations. From -your application's perspective, an OOME could also cause a system -crash. - -Some specific IMap API calls are particularly risky in this regard. -Methods like `IMap#entrySet()` and `IMap#values()` can trigger an OOME, depending -on the size of your map and the available memory on each member. -To mitigate this risk, you should follow these best practices. - -== Plan capacity -Proper capacity planning is crucial for providing -sufficient system resources to the Hazelcast cluster. This -involves estimating and validating the cluster's capacity -(memory, CPU, disk, etc.) to determine the best practices -that help the cluster achieve optimal performance. - -For more information, see xref:ROOT:capacity-planning.adoc[]. - -== Limit query result size -If you limit query result sizes, this can help prevent the adverse effects of bulk data reads. - -[source,java] ----- -Set> entrySet(); -Set> entrySet(Predicate predicate); ----- -For more information, see xref:data-structures:preventing-out-of-memory.adoc#configuring-query-result-size[Configuring query result size]. - -== Use Iterator -The Iterator fetches data in batches, ensuring consistent heap -utilization. The relevant methods in the IMap API include: - -[source,java] ----- -Iterator> iterator(); -Iterator> iterator(int fetchSize); ----- -This example shows how to use the Iterator API: -[source,java] ----- -IMap testMap = instance.getMap("test"); -for (int i = 0; i < 1_000; i++) { - testMap.set(i, i); -} - -// default fetch size is 100 element -Iterator> iterator = testMap.iterator(); -while (iterator.hasNext()) { - Map.Entry next = iterator.next(); - System.err.println(next); -} ----- - - -== Use PartitionPredicate -You can reduce memory overhead during bulk operations by filtering with *PartitionPredicate*. - -For more info, see xref:query:predicate-overview.adoc#filtering-with-partition-predicate[PartitionPredicate]. - -== Use Entry Processor -In some scenarios, reversing the traditional approach can be -more effective. Instead of fetching all data to the local -application for processing, you can send operations directly to -the data. This _in-place_ processing method saves both time and -resources; *Entry Processor* is an excellent tool for this purpose. - -For more info, see xref:data-structures:entry-processor.adoc[]. - -== Use SQL service -SQL was designed specifically for distributed computing use cases: SQL query results -are paged, which makes SQL a good tool to fetch data in bulk. - -The following example shows a replacement for `IMap#values()`: - -[source,java] ----- -String MAP_NAME = "..."; -HazelcastInstance client = HazelcastClient.newHazelcastClient(); -// Create a SQL mapping for IMap -client.getSql().execute("CREATE MAPPING " + MAP_NAME + " (__key INT, this VARCHAR)"); -// Run query to replace IMap#values() -SqlResult result = client.getSql().execute("SELECT this FROM " + MAP_NAME); -// Process the data in paged fashion -for (SqlRow row: result) { - /* do your processing */ -} ----- - -IMPORTANT: You must have Jet enabled to use the SQL service. - -For more info, see xref:query:sql-overview.adoc[]. - - From ddf3e1cae15bda02aeb38da9ee5a46c2d05ff587 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Or=C3=A7un=20=C3=87olak?= <90305879+orcunc@users.noreply.github.com> Date: Wed, 2 Oct 2024 15:43:40 +0300 Subject: [PATCH 11/16] Change documentation for "default" tag in map configuration (#1282) As the resolution of the SUP : https://hazelcast.atlassian.net/browse/SUP-550?focusedCommentId=102526 it was decided to update the documentation to make it clear that the "default" tag in map configuration means that it is used as "fallback" when the map's name that is being created does not match any entry --- .../data-structures/pages/map-config.adoc | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/docs/modules/data-structures/pages/map-config.adoc b/docs/modules/data-structures/pages/map-config.adoc index bee7e009c..4b24f5c87 100644 --- a/docs/modules/data-structures/pages/map-config.adoc +++ b/docs/modules/data-structures/pages/map-config.adoc @@ -4,6 +4,7 @@ {description} +[[map-configuration-defaults]] == Hazelcast Map Configuration Defaults The `hazelcast.xml`/`hazelcast.yaml` configuration included with your Hazelcast distribution includes the following default settings for maps. @@ -33,9 +34,22 @@ For details on map backups, refer to xref:backing-up-maps.adoc[]. For details on in-memory format, refer to xref:setting-data-format.adoc[]. -== Modifying the Default Configuration +== The Default (Fallback) Map Configuration +When a map is created, if the map name matches an entry in the `hazelcast.xml`/`hazelcast.yaml` file, the values in the matching entry are used to overwrite the initial values +discussed in the <> section. -You can create a default configuration for all maps for your environment by modifying the map configuration block named "default" in your `hazelcast.xml`/`hazelcast.yaml` file. In the following example, we set expiration timers for map entries. Map entries that are idle for an hour will be marked as eligible for removal if the cluster begins to run out of memory. Any map entry older than six hours will be marked as eligible for removal. +Maps that do not have any configuration defined use the default configuration. If you want to set a configuration that is valid for all maps, you can name your configuration as `default`. A user-defined default configuration applies to every map that does not have a specific custom map configuration defined with the map’s name. You can also use wildcards to associate your configuration with multiple map names. See the [configuration documentation](https://docs.hazelcast.com/hazelcast/5.5/configuration/using-wildcards) for more information about wildcards. + +When a map name does not match any entry in the `hazelcast.xml`/`hazelcast.yaml` file then: + +- If the `default` map configuration exists, the values under this entry are used to overwrite initial values. Therefore, `default` serves as a fallback. + +- If a `default` map configuration does not exist, the map is created with initial values as discussed in <>. + + +== Modifying the Default (Fallback) Configuration + +In the following example, we set expiration timers for dynamically created maps that lack a named configuration block. Map entries that are idle for an hour will be marked as eligible for removal if the cluster begins to run out of memory. Any map entry older than six hours will be marked as eligible for removal. For more on entry expiration, go to xref:managing-map-memory.adoc[Managing Map Memory]. From e72cc27205ce6b88da0e88273f5921259266f08f Mon Sep 17 00:00:00 2001 From: Patrick McGleenon Date: Wed, 2 Oct 2024 14:35:23 +0100 Subject: [PATCH 12/16] typo: removed reference to map connector (#1311) Removed the reference to map connector in the reliable topic page --- docs/modules/integrate/pages/reliable-topic-connector.adoc | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/docs/modules/integrate/pages/reliable-topic-connector.adoc b/docs/modules/integrate/pages/reliable-topic-connector.adoc index b0a898813..ec4d53306 100644 --- a/docs/modules/integrate/pages/reliable-topic-connector.adoc +++ b/docs/modules/integrate/pages/reliable-topic-connector.adoc @@ -5,8 +5,7 @@ used as a data sink within a pipeline. == Installing the Connector -The map connector is included in the full and slim -distributions of Hazelcast. +This connector is included in the full and slim distributions of Hazelcast. == Permissions [.enterprise]*{enterprise-product-name}* @@ -26,4 +25,4 @@ p.readFrom(TestSources.itemStream(100)) ``` A simple example is supplied above. For a more advanced version, also -see xref:pipelines:observables.adoc[Observables]. \ No newline at end of file +see xref:pipelines:observables.adoc[Observables]. From 5314007dce8719a65c7b5b6e2b0d89041c7e7871 Mon Sep 17 00:00:00 2001 From: Gareth Johnston <92283680+gareth-johnston@users.noreply.github.com> Date: Mon, 7 Oct 2024 14:23:05 +0100 Subject: [PATCH 13/16] Correct PassThroughMergePolicy documentation [DOC-174] (#1315) Per the code and javadoc this should read ".. unless the incoming entry is null." Javadoc : `Merges data structure entries from source to destination directly unless the merging entry is {@code null}` --- docs/modules/wan/pages/configuring-for-map-and-cache.adoc | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/modules/wan/pages/configuring-for-map-and-cache.adoc b/docs/modules/wan/pages/configuring-for-map-and-cache.adoc index a96cae6b2..691e107b9 100644 --- a/docs/modules/wan/pages/configuring-for-map-and-cache.adoc +++ b/docs/modules/wan/pages/configuring-for-map-and-cache.adoc @@ -88,7 +88,7 @@ the target map if it does not exist in the target map. * `HigherHitsMergePolicy`: Incoming entry merges from the source map to the target map if the source entry has more hits than the target one. * `PassThroughMergePolicy`: Incoming entry merges from the source map to -the target map unless the incoming entry is not null. +the target map unless the incoming entry is null. * `ExpirationTimeMergePolicy`: Incoming entry merges from the source map to the target map if the source entry will expire later than the destination entry. Please note that this merge policy can only be used when the clusters' clocks are in sync. @@ -169,7 +169,7 @@ the target cache if it does not exist in the target cache. * `HigherHitsMergePolicy`: Incoming entry merges from the source cache to the target cache if the source entry has more hits than the target one. * `PassThroughMergePolicy`: Incoming entry merges from the source cache to -the target cache unless the incoming entry is not null. +the target cache unless the incoming entry is null. * `ExpirationTimeMergePolicy`: Incoming entry merges from the source cache to the target cache if the source entry will expire later than the destination entry. Please note that this merge policy can only be used when the clusters' clocks are in sync. From 99cf5f19146149c0585bdaa64d22453ee2256a83 Mon Sep 17 00:00:00 2001 From: Jack Green Date: Mon, 7 Oct 2024 15:28:49 +0100 Subject: [PATCH 14/16] Use latest `setup-node` in GitHub Actions (#1316) Rather than specifying a specific `setup-node` version, and having to [manually increment it](https://github.com/hazelcast/hz-docs/pulls?q=actions%2Fsetup-node), instead we can specify the latest `v4` as we do with other GitHub Actions (e.g. `checkout`). --- .github/workflows/adoc-html.yml | 2 +- .github/workflows/validate.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/adoc-html.yml b/.github/workflows/adoc-html.yml index e68c81cef..1c71b531d 100644 --- a/.github/workflows/adoc-html.yml +++ b/.github/workflows/adoc-html.yml @@ -10,7 +10,7 @@ jobs: steps: - uses: actions/checkout@v4 - - uses: actions/setup-node@v4.0.2 + - uses: actions/setup-node@v4 with: node-version: 20 - name: Convert adoc diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml index 3ca1ce81d..870597156 100644 --- a/.github/workflows/validate.yml +++ b/.github/workflows/validate.yml @@ -14,7 +14,7 @@ jobs: steps: - uses: actions/checkout@v4 - - uses: actions/setup-node@v4.0.2 + - uses: actions/setup-node@v4 with: node-version: 20 - name: Check for broken internal links From 50480ef65f09e975697db479d86dcccfc863a901 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tomasz=20Gaw=C4=99da?= Date: Tue, 8 Oct 2024 14:50:22 +0200 Subject: [PATCH 15/16] Add Debezium 2.x docs (#1149) Co-authored-by: rebekah-lawrence <142301480+rebekah-lawrence@users.noreply.github.com> Co-authored-by: Oliver Howell --- docs/modules/ROOT/nav.adoc | 1 + .../integrate/pages/cdc-connectors.adoc | 234 ++++++++++++--- docs/modules/integrate/pages/connectors.adoc | 43 ++- .../pages/legacy-cdc-connectors.adoc | 99 +++++++ .../pipelines/pages/cdc-database-setup.adoc | 29 +- docs/modules/pipelines/pages/cdc-join.adoc | 266 +++++++++++++----- .../modules/pipelines/pages/cdc-postgres.adoc | 220 ++++++++++----- docs/modules/pipelines/pages/cdc.adoc | 190 +++++++------ 8 files changed, 809 insertions(+), 273 deletions(-) create mode 100644 docs/modules/integrate/pages/legacy-cdc-connectors.adoc diff --git a/docs/modules/ROOT/nav.adoc b/docs/modules/ROOT/nav.adoc index 758fba3e1..12ba9ae4b 100644 --- a/docs/modules/ROOT/nav.adoc +++ b/docs/modules/ROOT/nav.adoc @@ -194,6 +194,7 @@ include::wan:partial$nav.adoc[] * xref:integrate:database-connectors.adoc[Database & CDC Connectors] ** xref:integrate:jdbc-connector.adoc[] ** xref:integrate:cdc-connectors.adoc[] +** xref:integrate:legacy-cdc-connectors.adoc[] ** xref:integrate:elasticsearch-connector.adoc[] ** xref:integrate:mongodb-connector.adoc[] * File Connectors diff --git a/docs/modules/integrate/pages/cdc-connectors.adoc b/docs/modules/integrate/pages/cdc-connectors.adoc index dce0f675b..4f42298f4 100644 --- a/docs/modules/integrate/pages/cdc-connectors.adoc +++ b/docs/modules/integrate/pages/cdc-connectors.adoc @@ -1,4 +1,7 @@ = CDC Connector +[.enterprise]*Enterprise* + +NOTE: This page refers to Hazelcast's {enterprise-product-name} CDC connectors. For more information on {open-source-product-name} CDC connectors, see xref:integrate:legacy-cdc-connectors.adoc[]. Change Data Capture (CDC) refers to the process of observing changes made to a database and extracting them in a form usable by other @@ -8,52 +11,164 @@ Change Data Capture is especially important to Hazelcast, because it allows for the _streaming of changes from databases_, which can be efficiently processed by the Jet engine. -Implementation of CDC in Hazelcast is based on -link:https://debezium.io/[Debezium]. Hazelcast offers a generic Debezium source -which can handle CDC events from link:https://debezium.io/documentation/reference/stable/connectors/index.html[any database supported by Debezium], -but we're also striving to make CDC sources first class citizens in Hazelcast. -The ones for MySQL and PostgreSQL already are. +The implementation of CDC in Hazelcast {enterprise-product-name} is based on +link:https://debezium.io/[Debezium 2.x, window=_blank]. Hazelcast offers a generic Debezium source +which can handle CDC events from link:https://debezium.io/documentation/reference/2.7/connectors/index.html[any database supported by Debezium, window=_blank], +However, we're also striving to make CDC sources first class citizens in Hazelcast, +as we have done already for MySQL and PostgreSQL. + +== Install the CDC connector + +This connector is included in the full distribution of Hazelcast {enterprise-product-name}. + +=== Maven +To use this connector in a Maven project, add the following entries to the `` section of your `pom.xml` file: + +Generic connector: + +[source,xml] +---- + + com.hazelcast.jet + hazelcast-enterprise-cdc-debezium + {full-version} + +---- + +MySQL-specific connector: -== Installing the Connector +[source,xml] +---- + + com.hazelcast.jet + hazelcast-enterprise-cdc-mysql + {full-version} + +---- +NOTE: Due to licensing, MySQL connector does not include the MySQL driver as a dependency. You have to manually add the `com.mysql:mysql-connector-j` dependency to the classpath. -This connector is included in the full and slim distributions of Hazelcast. +PostgreSQL-specific connector: -== CDC as a Source +[source,xml] +---- + + com.hazelcast.jet + hazelcast-enterprise-cdc-postgres + {full-version} + +---- -We have the following types of CDC sources: +== CDC as a source -* link:https://docs.hazelcast.org/docs/{full-version}/javadoc/com/hazelcast/jet/cdc/DebeziumCdcSources.html[DebeziumCdcSources]: - generic source for all databases supported by Debezium -* link:https://docs.hazelcast.org/docs/{full-version}/javadoc/com/hazelcast/jet/cdc/mysql/MySqlCdcSources.html[MySqlCdcSources]: - specific, first class Jet CDC source for MySQL databases (also based - on Debezium, but benefiting the full range of convenience Jet can - additionally provide) -* link:https://docs.hazelcast.org/docs/{full-version}/javadoc/com/hazelcast/jet/cdc/postgres/PostgresCdcSources.html[PostgresCdcSources]: - specific, first class CDC source for PostgreSQL databases (also based - on Debezium, but benefiting the full range of convenience Hazelcast can - additionally provide) +The Java API supports the following types of CDC source: -For the setting up a streaming source of CDC data is just the matter of pointing it at the right database via configuration: +* link:https://docs.hazelcast.org/docs/{full-version}/javadoc/com/hazelcast/enterprise/jet/cdc/DebeziumCdcSources.html[DebeziumCdcSources, window=_blank]: + a generic source for all databases supported by Debezium +* link:https://docs.hazelcast.org/docs/{full-version}/javadoc/com/hazelcast/enterprise/jet/cdc/mysql/MySqlCdcSources.html[MySqlCdcSources, window=_blank]: + a specific, first class Jet CDC source for MySQL databases (also based + on Debezium, but with the additional benefits provided by Hazelcast) +* link:https://docs.hazelcast.org/docs/{full-version}/javadoc/com/hazelcast/enterprise/jet/cdc/postgres/PostgresCdcSources.html[PostgresCdcSources, window=_blank]: + a specific, first class CDC source for PostgreSQL databases (also based +on Debezium, but with the additional benefits provided by Hazelcast) -```java +To set up a CDC data streaming source, define it using the following configuration: + +[tabs] +==== +MySQL:: ++ +-- +[source,java] +---- Pipeline pipeline = Pipeline.create(); pipeline.readFrom( MySqlCdcSources.mysql("customers") - .setDatabaseAddress("127.0.0.1") - .setDatabasePort(3306) - .setDatabaseUser("debezium") - .setDatabasePassword("dbz") + .setDatabaseAddress("127.0.0.1", 3306) + .setDatabaseCredentials("debezium", "dbz") .setClusterName("dbserver1") - .setDatabaseWhitelist("inventory") - .setTableWhitelist("inventory.customers") + .setDatabaseIncludeList("inventory") + .setTableIncludeList("inventory.customers") .build()) .withNativeTimestamps(0) .writeTo(Sinks.logger()); -``` +---- +-- +PostgreSQL:: ++ +-- +[source,java] +---- +Pipeline pipeline = Pipeline.create(); +pipeline.readFrom( + PostgresCdcSources.postgres("customers") + .setDatabaseAddress("127.0.0.1", 5432) + .setDatabaseCredentials("debezium", "dbz") + .setClusterName("dbserver1") + .setDatabaseIncludeList("inventory") + .setTableIncludeList("inventory.customers") + .build()) + .withNativeTimestamps(0) + .writeTo(Sinks.logger()); +---- +-- +MongoDB:: ++ +-- +[source,java] +---- +Pipeline pipeline = Pipeline.create(); +pipeline.readFrom( + DebeziumCdcSources.debezium("customers", MongoDbConnector.class) + .setProperty("mongodb.connection.string", "mongodb://localhost:27017") + .setDatabaseIncludeList("inventory") + .setProperty("collection.include.list", "customers") + .build()) + .withNativeTimestamps(0) + .writeTo(Sinks.logger()); +---- +-- + +==== + +MySQL- and PostgreSQL-specific source builders contain methods for all major configuration settings with protection if, for example, mutually exclusive options are not used. If using a generic source builder, refer to the link:https://debezium.io/documentation/reference/stable/index.html[Debezium, window=_blank] documentation for the information about required or mutually exclusive fields. + +Follow the provided xref:pipelines:cdc.adoc[] tutorial to see how CDC processes change events from a MySQL database. + +[NOTE] +==== +Remember to ensure your database is up and running before a CDC job is started, including any additional required CDC agents (as required by DB2), for example. +==== + +=== Common source builder functions +[cols="m,a"] +|=== +|Method name|Description + +|changeRecord() +| Sets output type to `ChangeRecord` - a wrapper, which provides most of the fields in +a strongly-typed manner. + +| json() +| Sets output type to `JSON` - in the result stage, the type will be set to `Map`, +where the map entry's key is the key of `SourceRecord` in JSON format, and the value is the whole `SourceRecord`'s value in JSON format. + +|customMapping(RecordMappingFunction) +| Sets the output type to an arbitrary user type, `T`. Mapping from `SourceRecord` to `T` is done using the function provided by the connector. + +|withDefaultEngine() +|Sets the preferred engine to the default (non-async) one. This engine is single-threaded, +but also more widely used and tested. Use this engine for the most stable results (for example, no async offset restore). For MySQL and PostgreSQL especially this engine makes the most sense, as MySQL and PostgreSQL Debezium connectors are single-threaded only. + +|withAsyncEngine() +|Sets the preferred engine to the async one. This engine is multithreaded (if supported by the connector), but be aware of the async nature; for example, offset restore may occur asynchronously after the restart is done, leading to sometimes confusing results. -For an example of how to use CDC data see xref:pipelines:cdc.adoc[our tutorial]. +|setProperty(String, String) +|Sets connector property to given value. There are multiple overloads, allowing to +set the value to `long`, `String` or `boolean`. -=== Fault Tolerance +|=== + +=== Fault tolerance CDC sources offer at least-once processing guarantees. The source periodically saves the database write ahead log offset for which it had @@ -79,20 +194,71 @@ For example, a sink mapping CDC data to a `Customer` class and maintaining a map view of latest known email addresses per customer (identified by ID) would look like this: -```java +[source,java] +---- Pipeline p = Pipeline.create(); p.readFrom(source) .withoutTimestamps() .writeTo(CdcSinks.map("customers", r -> r.key().toMap().get("id"), r -> r.value().toObject(Customer.class).email)); -``` +---- [NOTE] ==== The key and value functions have certain limitations. They can be used to map only to objects which the Hazelcast member can deserialize, which unfortunately doesn't include user code submitted as a part of the job. So in the above example it's OK to have `String` email values, but we wouldn't be able to use `Customer` directly. If user code has to be used, then the problem can be solved with the help of the User Code Deployment feature. Example configs for that can be seen in our xref:pipelines:cdc-join.adoc#7-start-hazelcast-jet[CDC Join tutorial]. +==== + +== Data types + +Hazelcast relies on Debezium, which in turn uses the Kafka Connect API, including `Struct` objects for example. Hazelcast makes conversion to `Map` and `POJO`s easier by providing abstractions such as `RecordPart`. Despite this, it's worth knowing how some database types can or will be mapped to Java types. + +[NOTE] +==== +Each database type has its own database type-to-struct type mappings. For specific mappings of this type, see the Debezium documentation, for example: link:https://debezium.io/documentation/reference/stable/connectors/mysql.html#mysql-data-types[MySQL], link:https://debezium.io/documentation/reference/stable/connectors/postgresql.html#postgresql-data-types[PostgreSQL], link:https://debezium.io/documentation/reference/stable/connectors/db2.html#db2-data-types[DB2], etc.. +==== + +=== Common datatypes mapping. +[cols="m,a,a"] +|=== +|Struct type|Semantic type|Java type + +.3+|INT32 +|-|int/Integer +|io.debezium.time.Date|java.time.LocalDate / java.util.Date / String `yyyy-MM-dd` +|io.debezium.time.Time|java.time.Duration / String ISO-8601 `PnDTnHnMn.nS` + +.5+|INT64 +|-|long/Long +|io.debezium.time.Timestamp|java.time.Instant / String `yyyy-MM-dd HH:mm:ss.SSS` +|io.debezium.time.MicroTimestamp|java.time.Instant / String `yyyy-MM-dd HH:mm:ss.SSS` +|io.debezium.time.MicroTime|java.time.Duration / String ISO-8601 `PnDTnHnMn.nS` +|io.debezium.time.NanoTimestamp|java.time.Instant / String `yyyy-MM-dd HH:mm:ss.SSS` +|io.debezium.time.NanoTime|java.time.Duration / String ISO-8601 `PnDTnHnMn.nS` + +|FLOAT32|-|float/Float / String +|FLOAT64|-|double/Double / String +|BOOLEAN|-|boolean/Boolean / String +|STRING|-|String + +The `RecordPart#value` field contains Debezium's message in a JSON format. This JSON format uses string as date representation, +instead of ints, which are standard in Debezium but harder to work with. + +[NOTE] +==== +We strongly recommend using `time.precision.mode=adaptive` (default). +Using `time.precision.mode=connect` uses `java.util.Date` to represent dates, time, etc. and is less precise. +==== + +|=== + +== Migration tips + +Hazelcast {open-source-product-name} has a Debezium CDC connector, but it's based on an older version of Debezium. +Migration to the new connector is straightforward but be aware of the following changes: -Although User Code Deployment has been deprecated, the replacement User Code Namespaces feature does not yet support Jet jobs or pipelines. For now, continue to use the User Code Deployment solution in this scenario. -==== \ No newline at end of file + * You should use the `com.hazelcast.enterprise.jet.cdc` package instead of `com.hazelcast.jet.cdc`. + * Artifact names are now `hazelcast-enterprise-cdc-debezium`, `hazelcast-enterprise-cdc-mysql` and `hazelcast-enterprise-cdc-postgres` (instead of `hazelcast-jet-...`). + * Debezium renamed certain terms, which we have also replicated in our code. For example, `include list` replaces `whitelist`, `exclude list` replaces `blacklist`. This means, for example, you need to use `setTableIncludeList` instead of `setTableWhitelist`. For more detail on new Debezium names, see their link:https://debezium.io/documentation/reference/stable/connectors/mysql.html#mysql-connector-properties[MySQL] and link:https://debezium.io/documentation/reference/stable/connectors/postgresql.html#postgresql-connector-properties[PostgreSQL] documentation. \ No newline at end of file diff --git a/docs/modules/integrate/pages/connectors.adoc b/docs/modules/integrate/pages/connectors.adoc index 28ffb43d9..875ef8b1e 100644 --- a/docs/modules/integrate/pages/connectors.adoc +++ b/docs/modules/integrate/pages/connectors.adoc @@ -115,11 +115,36 @@ The Jet API supports more connectors than SQL. |batch |N/A -|xref:integrate:cdc-connectors.adoc[DebeziumCdcSources.debezium] +|xref:integrate:legacy-cdc-connectors.adoc[DebeziumCdcSources.debezium] (Legacy) |hazelcast-jet-cdc-debezium |streaming |at-least-once +|xref:integrate:legacy-cdc-connectors.adoc[MySqlCdcSources.mysql] (Legacy) +|hazelcast-jet-cdc-mysql +|streaming +|exactly-once + +|xref:integrate:legacy-cdc-connectors.adoc[PostgresCdcSources.postgres] (Legacy) +|hazelcast-jet-cdc-postgres +|streaming +|exactly-once + +|xref:integrate:cdc-connectors.adoc[DebeziumCdcSources.debezium] ([.enterprise]*Enterprise*) +|hazelcast-enterprise-cdc-debezium +|streaming +|at-least-once + +|xref:integrate:cdc-connectors.adoc[MySqlCdcSources.mysql] +|hazelcast-enterprise-cdc-mysql +|streaming +|exactly-once + +|xref:integrate:cdc-connectors.adoc[PostgresCdcSources.postgres] +|hazelcast-enterprise-cdc-postgres +|streaming +|exactly-once + |xref:integrate:elasticsearch-connector.adoc[ElasticSources.elastic] |hazelcast-jet-elasticsearch-7 |batch @@ -150,16 +175,6 @@ The Jet API supports more connectors than SQL. |streaming |exactly-once -|xref:integrate:cdc-connectors.adoc[MySqlCdcSources.mysql] -|hazelcast-jet-cdc-mysql -|streaming -|exactly-once - -|xref:integrate:cdc-connectors.adoc[PostgresCdcSources.postgres] -|hazelcast-jet-cdc-postgres -|streaming -|exactly-once - |xref:integrate:pulsar-connector.adoc[PulsarSources.pulsarConsumer] |hazelcast-jet-contrib-pulsar |streaming @@ -270,7 +285,11 @@ The Jet API supports more connectors than SQL. |N/A |xref:integrate:cdc-connectors.adoc[CdcSinks.map] -|hazelcast-jet-cdc-debezium +|hazelcast-jet-cdc-debezium (legacy, {open-source-product-name}) + +or + +hazelcast-enterprise-cdc-debezium ({enterprise-product-name}) |streaming |at-least-once diff --git a/docs/modules/integrate/pages/legacy-cdc-connectors.adoc b/docs/modules/integrate/pages/legacy-cdc-connectors.adoc new file mode 100644 index 000000000..662789fcf --- /dev/null +++ b/docs/modules/integrate/pages/legacy-cdc-connectors.adoc @@ -0,0 +1,99 @@ += Legacy CDC Connector + + +NOTE: This page refers to Hazelcast's {open-source-product-name} CDC connectors, also known as legacy CDC connectors. For more information on {enterprise-product-name} CDC connectors, see xref:integrate:cdc-connectors.adoc[]. + +Change Data Capture (CDC) refers to the process of observing changes +made to a database and extracting them in a form usable by other +systems, for the purposes of replication, analysis and many more. + +Change Data Capture is especially important to Hazelcast, because it allows +for the _streaming of changes from databases_, which can be efficiently +processed by the Jet engine. + +The implementation of CDC in Hazelcast {open-source-product-name} is based on +link:https://debezium.io/[Debezium, window=_blank]. Hazelcast offers a generic Debezium source +that can handle CDC events from link:https://debezium.io/documentation/reference/stable/connectors/index.html[any database supported by Debezium, window=_blank]. +However, we're also striving to make CDC sources first class citizens in Hazelcast, +as we have done already for MySQL and PostgreSQL. + +== Install the CDC connector + +This connector is included in the full distribution of Hazelcast {open-source-product-name}. + +== CDC as a source + +We have the following types of CDC sources: + +* link:https://docs.hazelcast.org/docs/{full-version}/javadoc/com/hazelcast/jet/cdc/DebeziumCdcSources.html[DebeziumCdcSources, window=_blank]: + a generic source for all databases supported by Debezium +* link:https://docs.hazelcast.org/docs/{full-version}/javadoc/com/hazelcast/jet/cdc/mysql/MySqlCdcSources.html[MySqlCdcSources, window=_blank]: + a specific, first class Jet CDC source for MySQL databases (also based + on Debezium, but with the additional benefits provided by Hazelcast) +* link:https://docs.hazelcast.org/docs/{full-version}/javadoc/com/hazelcast/jet/cdc/postgres/PostgresCdcSources.html[PostgresCdcSources, window=_blank]: + a specific, first class CDC source for PostgreSQL databases (also based + on Debezium, but with the additional benefits provided by Hazelcast) + +To set up a streaming source of CDC data, define it using the following configuration: + +[source,java] +---- +Pipeline pipeline = Pipeline.create(); +pipeline.readFrom( + MySqlCdcSources.mysql("customers") + .setDatabaseAddress("127.0.0.1") + .setDatabasePort(3306) + .setDatabaseUser("debezium") + .setDatabasePassword("dbz") + .setClusterName("dbserver1") + .setDatabaseWhitelist("inventory") + .setTableWhitelist("inventory.customers") + .build()) + .withNativeTimestamps(0) + .writeTo(Sinks.logger()); +---- + +For an example of how to use CDC data, see the xref:pipelines:cdc.adoc[] tutorial. + +=== Fault tolerance + +CDC sources offer _at least once_ processing guarantees. The source +periodically saves the database write ahead log offset for which it has +dispatched events and in case of a failure/restart it will replay all +events since the last successfully saved offset. + +Unfortunately, there is no guarantee that the last saved offset +is still in the database changelog. Such logs are always finite and, +depending on the DB configuration, can be relatively short, so if the CDC +source has to replay data for a long period of inactivity, then there +can be data loss. With careful management, the _at least once_ guarantee can be practically implemented. + +== CDC as a sink + +Change data capture is a source-side functionality in Jet, but Hazelcast also +offers specialized sinks that simplify applying CDC events to a map, which gives you the ability to reconstruct the contents of the original database table. The sinks expect to receive `ChangeRecord` +objects and apply your custom functions to them that extract the key and +the value that will be applied to the target map. + +For example, a sink mapping CDC data to a `Customer` class and +maintaining a map view of latest known email addresses per customer +(identified by ID) would look like this: + +[source,java] +---- +Pipeline p = Pipeline.create(); +p.readFrom(source) + .withoutTimestamps() + .writeTo(CdcSinks.map("customers", + r -> r.key().toMap().get("id"), + r -> r.value().toObject(Customer.class).email)); +---- + +[NOTE] +==== +The key and value functions have certain limitations. They can be used to map only to objects which the Hazelcast member can deserialize, which unfortunately doesn't include user code submitted as a part of the job. So in the above example it's OK to have `String` email values, but we wouldn't be able to use `Customer` directly. + +If user code has to be used, then the problem can be solved with the help of the User Code Deployment feature. Example configs for that can be seen in our xref:pipelines:cdc-join.adoc#7-start-hazelcast-jet[CDC Join tutorial]. + +Although User Code Deployment has been deprecated, the replacement User Code Namespaces feature does not yet support Jet jobs or pipelines. For now, continue to use the User Code Deployment solution in this scenario. +==== \ No newline at end of file diff --git a/docs/modules/pipelines/pages/cdc-database-setup.adoc b/docs/modules/pipelines/pages/cdc-database-setup.adoc index 22877d4c8..69e0c75e0 100644 --- a/docs/modules/pipelines/pages/cdc-database-setup.adoc +++ b/docs/modules/pipelines/pages/cdc-database-setup.adoc @@ -39,13 +39,14 @@ config file. See MySQL Reference Manual on how to do that link:https://dev.mysql.com/doc/refman/8.0/en/option-files.html[8.0]). For example: -``` +[source] +---- server-id = 223344 log_bin = mysql-bin binlog_format = ROW binlog_row_image = FULL expire_logs_days = 10 -``` +---- The semantics of these options are as follows: @@ -62,9 +63,10 @@ link:https://dev.mysql.com/doc/refman/8.0/en/show-variables.html[8.0]). It's worth pointing out that the names of the options sometimes differ from the names of the MySQL system variables they set. For example: -``` +[source] +---- SHOW VARIABLES LIKE 'server_id'; -``` +---- === Configure Session Timeouts @@ -121,7 +123,8 @@ configuration options to be set accordingly. This can be done either by The important properties to set are: -```properties +[source,properties] +---- # MODULES shared_preload_libraries = 'decoderbufs,wal2json' @@ -129,7 +132,7 @@ shared_preload_libraries = 'decoderbufs,wal2json' wal_level = logical max_wal_senders = 1 max_replication_slots = 1 -``` +---- `shared_preload_libraries` contains a comma separated list of installed output plug-ins. `wal_levels` is used to tell the server to use logical @@ -152,9 +155,10 @@ permissions. The permissions needed are `REPLICATION` and `LOGIN`. For setting up database users/roles see the link:https://www.postgresql.org/docs/9.6/user-manag.html[PostgreSQL documentation], but basically the essential command is: -``` +[source,sql] +---- CREATE ROLE name REPLICATION LOGIN; -``` +---- Note: database super-users already have all the permissions needed by replication too. @@ -166,11 +170,12 @@ PostgreSQL server needs to allow access from the host the CDC connector is running on. To specify such link:https://www.postgresql.org/docs/9.6/auth-pg-hba-conf.html[client authentication] options add following lines to the end of the `pg_hba.conf` file: -``` +[source] +---- local replication user trust host replication user 127.0.0.1/32 trust host replication user ::1/128 trust -``` +---- This example tells the server to allow replication for the specified user locally or on `localhost`, using IPv4 or IPv6. @@ -250,9 +255,9 @@ In our tests we didn't manage to make it output much more than 20,000 records/second, so on a powerful server running the database it shouldn't affect normal operations too severely. -==== Failure Tolerance +==== Failure tolerance -PostgreSQL failure tolerance associated with replication slots is +PostgreSQL's failure tolerance associated with replication slots is somewhat lacking in certain aspects. The CDC connector can quite nicely deal with its own restart or connection loss to the primary database, but only as long as replication slots remain intact. Replication diff --git a/docs/modules/pipelines/pages/cdc-join.adoc b/docs/modules/pipelines/pages/cdc-join.adoc index 448ad895f..d5fca7f64 100644 --- a/docs/modules/pipelines/pages/cdc-join.adoc +++ b/docs/modules/pipelines/pages/cdc-join.adoc @@ -3,6 +3,8 @@ In this tutorial, you will learn how to make a map hold enriched data, combined (joined) from multiple database tables. +NOTE: If you are using Hazelcast {open-source-product-name}, you have to change the package from `com.hazelcast.enterprise.jet...` to `com.hazelcast.jet...`. + == Step 1. Install Docker This tutorial uses link:https://www.docker.com/[Docker] to simplify the @@ -45,7 +47,70 @@ They differ slightly depending on which database you use: Let's write the code for the processing we want to accomplish: -```java +[tabs] +==== +{enterprise-product-name}:: ++ +-- +[source,java] +---- +package org.example; + +import com.hazelcast.core.Hazelcast; +import com.hazelcast.core.HazelcastInstance; +import com.hazelcast.enterprise.jet.cdc.ChangeRecord; +import com.hazelcast.enterprise.jet.cdc.mysql.MySqlCdcSources; +import com.hazelcast.jet.config.JobConfig; +import com.hazelcast.jet.pipeline.Pipeline; +import com.hazelcast.jet.pipeline.Sinks; +import com.hazelcast.jet.pipeline.StreamSource; +import com.hazelcast.jet.pipeline.StreamStage; + +public class JetJob { + +private static final int MAX_CONCURRENT_OPERATIONS = 1; + +public static void main(String[] args) { + StreamSource source = MySqlCdcSources.mysql("source") + .setDatabaseAddress("127.0.0.1", 3306) + .setDatabaseCredentials("debezium", "dbz") + .setClusterName("dbserver1") + .setDatabaseIncludeList("inventory") + .setTableIncludeList("inventory.customers", "inventory.orders") + .build(); + + Pipeline pipeline = Pipeline.create(); + StreamStage allRecords = pipeline.readFrom(source) + .withNativeTimestamps(0); + + allRecords.filter(r -> r.table().equals("customers")) + .apply(Ordering::fix) + .peek() + .writeTo(Sinks.mapWithEntryProcessor(MAX_CONCURRENT_OPERATIONS, "cache", + record -> (Integer) record.key().toMap().get("id"), + CustomerEntryProcessor::new + )); + + allRecords.filter(r -> r.table().equals("orders")) + .apply(Ordering::fix) + .peek() + .writeTo(Sinks.mapWithEntryProcessor(MAX_CONCURRENT_OPERATIONS, "cache", + record -> (Integer) record.value().toMap().get("purchaser"), + OrderEntryProcessor::new + )); + + JobConfig cfg = new JobConfig().setName("monitor"); + HazelcastInstance hz = Hazelcast.bootstrappedInstance(); + hz.getJet().newJob(pipeline, cfg); + } +} +---- +-- +{open-source-product-name}:: ++ +-- +[source,java] +---- package org.example; import com.hazelcast.core.Hazelcast; @@ -64,10 +129,8 @@ private static final int MAX_CONCURRENT_OPERATIONS = 1; public static void main(String[] args) { StreamSource source = MySqlCdcSources.mysql("source") - .setDatabaseAddress("127.0.0.1") - .setDatabasePort(3306) - .setDatabaseUser("debezium") - .setDatabasePassword("dbz") + .setDatabaseAddress("127.0.0.1", 3306) + .setDatabaseCredentials("debezium", "dbz") .setClusterName("dbserver1") .setDatabaseWhitelist("inventory") .setTableWhitelist("inventory.customers", "inventory.orders") @@ -98,30 +161,50 @@ public static void main(String[] args) { hz.getJet().newJob(pipeline, cfg); } } -``` +---- +-- +==== If using Postgres, only the source would need to change, like this: -```java +[tabs] +==== +{enterprise-product-name}:: ++ +[source,java] +---- +StreamSource source = PostgresCdcSources.postgres("source") + .setDatabaseAddress("127.0.0.1", 5432) + .setDatabaseCredentials("postgres", "postgres") + .setDatabaseName("postgres") + .setTableIncludeList("inventory.customers", "inventory.orders") + .build(); +---- +{open-source-product-name}:: ++ +[source,java] +---- StreamSource source = PostgresCdcSources.postgres("source") .setDatabaseAddress("127.0.0.1") .setDatabasePort(5432) .setDatabaseUser("postgres") .setDatabasePassword("postgres") .setDatabaseName("postgres") - .setTableWhitelist("inventory.customers", "inventory.orders") + .setTableIncludeList("inventory.customers", "inventory.orders") .build(); -``` +---- +==== As we can see from the pipeline code, our `Sink` is `EntryProcessor` based. The two `EntryProcessors` we use are: -```java +[source,java] +---- package org.example; -import com.hazelcast.jet.cdc.ChangeRecord; -import com.hazelcast.jet.cdc.Operation; -import com.hazelcast.jet.cdc.ParsingException; +import com.hazelcast.enterprise.jet.cdc.ChangeRecord; +import com.hazelcast.enterprise.jet.cdc.Operation; +import com.hazelcast.enterprise.jet.cdc.ParsingException; import com.hazelcast.map.EntryProcessor; import java.util.Map; @@ -158,14 +241,15 @@ public class CustomerEntryProcessor implements EntryProcessor/build/libs/cdc-tutorial-1.0-SNAPSHOT.jar -``` +---- -- Maven:: + -- -```yaml +[source,yaml] +---- user-code-deployment: enabled: true jarPaths: - /target/cdc-tutorial-1.0-SNAPSHOT.jar -``` +---- -- ==== + @@ -558,17 +669,19 @@ you created the project for this tutorial. . Start Hazelcast. + -```bash +[source,bash] +---- bin/hz-start -``` +---- . When you see output like this, Hazelcast is up: + -``` +[source] +---- Members {size:1, ver:1} [ Member [192.168.1.5]:5701 - e7c26f7c-df9e-4994-a41d-203a1c63480e this ] -``` +---- == Step 8. Submit the Job for Execution @@ -581,77 +694,85 @@ following command: Gradle:: + -- -```bash +[source,bash] +---- bin/hz-cli submit build/libs/cdc-tutorial-1.0-SNAPSHOT.jar -``` +---- -- Maven:: + -- -```bash +[source,bash] +---- bin/hz-cli submit target/cdc-tutorial-1.0-SNAPSHOT.jar -``` +---- -- ==== The output in the Hazelcast member's log should look something like this (we see these lines due to the `peek()` stages we've inserted): -``` +[source,text] +---- ........ ... Output to ordinal 0: key:{{"order_number":10002}}, value:{{"order_number":10002,"order_date":16817,"purchaser":1002,"quantity":2,"product_id":105,"__op":"c","__db":"inventory","__table":"orders","__ts_ms":1593681751174,"__deleted":"false"}} (eventTime=12:22:31.174) ... Output to ordinal 0: key:{{"order_number":10003}}, value:{{"order_number":10003,"order_date":16850,"purchaser":1002,"quantity":2,"product_id":106,"__op":"c","__db":"inventory","__table":"orders","__ts_ms":1593681751174,"__deleted":"false"}} (eventTime=12:22:31.174) ... Output to ordinal 0: key:{{"id":1003}}, value:{{"id":1003,"first_name":"Edward","last_name":"Walker","email":"ed@walker.com","__op":"c","__db":"inventory","__table":"customers","__ts_ms":1593681751161,"__deleted":"false"}} (eventTime=12:22:31.161) ........ -``` +---- == Step 9. Track Updates Let's see how our cache looks like at this time. If we execute the `CacheRead` code <<5-define-jet-job, defined above>>, we'll get: -``` +[source] +---- Currently there are following customers in the cache: Customer: Customer {id=1002, firstName=George, lastName=Bailey, email=gbailey@foobar.com}, Orders: {10002=Order {orderNumber=10002, orderDate=Sun Jan 17 02:00:00 EET 2016, purchaser=1002, quantity=2, productId=105}, 10003=Order {orderNumber=10003, orderDate=Fri Feb 19 02:00:00 EET 2016, purchaser=1002, quantity=2, productId=106}} Customer: Customer {id=1003, firstName=Edward, lastName=Walker, email=ed@walker.com}, Orders: {10004=Order {orderNumber=10004, orderDate=Sun Feb 21 02:00:00 EET 2016, purchaser=1003, quantity=1, productId=107}} Customer: Customer {id=1004, firstName=Anne, lastName=Kretchmar, email=annek@noanswer.org}, Orders: {} Customer: Customer {id=1001, firstName=Sally, lastName=Thomas, email=sally.thomas@acme.com}, Orders: {10001=Order {orderNumber=10001, orderDate=Sat Jan 16 02:00:00 EET 2016, purchaser=1001, quantity=1, productId=102}} -``` +---- Let's do some updates in our database. Go to the database CLI <<3-start-command-line-client, we've started earlier>> and run following commands: -```bash +[source,sql] +---- INSERT INTO inventory.customers VALUES (1005, 'Jason', 'Bourne', 'jason@bourne.org'); DELETE FROM inventory.orders WHERE order_number=10002; -``` +---- If we check the cache with `CacheRead` we get: -``` +[source] +---- Currently there are following customers in the cache: Customer: Customer {id=1005, firstName=Jason, lastName=Bourne, email=jason@bourne.org}, Orders: {} Customer: Customer {id=1002, firstName=George, lastName=Bailey, email=gbailey@foobar.com}, Orders: {10003=Order {orderNumber=10003, orderDate=Fri Feb 19 02:00:00 EET 2016, purchaser=1002, quantity=2, productId=106}} Customer: Customer {id=1003, firstName=Edward, lastName=Walker, email=ed@walker.com}, Orders: {10004=Order {orderNumber=10004, orderDate=Sun Feb 21 02:00:00 EET 2016, purchaser=1003, quantity=1, productId=107}} Customer: Customer {id=1004, firstName=Anne, lastName=Kretchmar, email=annek@noanswer.org}, Orders: {} Customer: Customer {id=1001, firstName=Sally, lastName=Thomas, email=sally.thomas@acme.com}, Orders: {10001=Order {orderNumber=10001, orderDate=Sat Jan 16 02:00:00 EET 2016, purchaser=1001, quantity=1, productId=102}} -``` +---- == Step 10. Clean up . Cancel the job. + -```bash +[source,bash] +---- bin/hz-cli cancel postgres-monitor -``` +---- Shut down the Hazelcast cluster. + -```bash +[source,bash] +---- bin/hz-stop -``` +---- . Use Docker to stop the running container (this will kill the command-line client too, since it's running in the same container): @@ -663,9 +784,10 @@ MySQL:: -- You can use Docker to stop all running containers: -```bash +[source,bash] +---- docker stop mysqlterm mysql -``` +---- -- Postgres:: + @@ -674,9 +796,10 @@ Postgres:: You can use Docker to stop the running container (this will kill the command-line client too, since it's running in the same container): -```bash +[source,bash] +---- docker stop postgres -``` +---- -- ==== + @@ -685,6 +808,7 @@ Docker should remove them right after we stop them. We can verify that all processes are stopped and removed with following command: -```bash +[source,bash] +---- docker ps -a -``` +---- diff --git a/docs/modules/pipelines/pages/cdc-postgres.adoc b/docs/modules/pipelines/pages/cdc-postgres.adoc index ca77ddbc0..ed9e6fe1f 100644 --- a/docs/modules/pipelines/pages/cdc-postgres.adoc +++ b/docs/modules/pipelines/pages/cdc-postgres.adoc @@ -23,11 +23,12 @@ Open a terminal, and run following command. It will start a new container that runs a PostgreSQL database server preconfigured with an inventory database: -```bash +[source,bash] +---- docker run -it --rm --name postgres -p 5432:5432 \ -e POSTGRES_DB=postgres -e POSTGRES_USER=postgres \ -e POSTGRES_PASSWORD=postgres debezium/example-postgres:1.2 -``` +---- This runs a new container using version `1.2` of the link:https://hub.docker.com/r/debezium/example-postgres[debezium/example-postgres] @@ -50,7 +51,8 @@ container to the same port on the Docker host so that software outside In your terminal you should see something like the following: -``` +[source] +---- ... PostgreSQL init process complete; ready for start up. @@ -58,7 +60,7 @@ PostgreSQL init process complete; ready for start up. 2020-06-02 11:36:19.581 GMT [1] LOG: listening on IPv6 address "::", port 5432 2020-06-02 11:36:19.585 GMT [1] LOG: listening on Unix socket "/var/run/postgresql/.s.PGSQL.5432" 2020-06-02 11:36:19.618 GMT [1] LOG: database system is ready to accept connections -``` +---- The PostgreSQL server is running and ready for use. @@ -67,35 +69,40 @@ The PostgreSQL server is running and ready for use. Open a new terminal, and use it to run `psql` (PostgreSQL interactive terminal) inside the already running `postgres` container: -```bash +[source,bash] +---- docker exec -it postgres psql -U postgres -``` +---- You should end up with a prompt similar to this: -``` +[source] +---- psql (11.8 (Debian 11.8-1.pgdg90+1)) Type "help" for help. postgres=# -``` +---- We’ll use the prompt to interact with the database. First, switch to the "inventory" schema: -```bash +[source,bash] +---- SET search_path TO inventory; -``` +---- and then list the tables in the database: -```bash +[source,bash] +---- \dt; -``` +---- This should display the following: -``` +[source] +------------ List of relations Schema | Name | Type | Owner -----------+------------------+-------+---------- @@ -106,14 +113,15 @@ This should display the following: inventory | products_on_hand | table | postgres inventory | spatial_ref_sys | table | postgres (6 rows) -``` +------------ Feel free to explore the database and view the preloaded data. For example: -```bash +[source,bash] +---- SELECT * FROM customers; -``` +---- == Step 4. Start Hazelcast @@ -124,28 +132,44 @@ follow from here on. . Make sure the PostgreSQL CDC plugin is in the `lib/` directory. + -```bash +[source,bash] +---- ls lib/ -``` +---- + You should see the following jars: + +[tabs] +==== +{enterprise-product-name}:: ++ +-- +* hazelcast-enterprise-cdc-debezium-{full-version}.jar +* hazelcast-enterprise-cdc-postgres-{full-version}.jar (for Postgres) +-- +{open-source-product-name}:: ++ +-- * hazelcast-jet-cdc-debezium-{full-version}.jar -* hazelcast-jet-cdc-postgres-{full-version}.jar +* hazelcast-jet-cdc-postgres-{full-version}.jar (for Postgres) +-- +==== . Start Hazelcast + -```bash +[source,bash] +---- bin/hz-start -``` +---- . When you see output like this, Hazelcast is up: + -``` +[source] +---- Members {size:1, ver:1} [ Member [192.168.1.5]:5701 - e7c26f7c-df9e-4994-a41d-203a1c63480e this ] -``` +---- == Step 5. Create a New Java Project @@ -171,8 +195,8 @@ repositories.mavenCentral() dependencies { implementation 'com.hazelcast:hazelcast:{full-version}' - implementation 'com.hazelcast.jet:hazelcast-jet-cdc-debezium:{full-version}' - implementation 'com.hazelcast.jet:hazelcast-jet-cdc-postgres:{full-version}' + implementation 'com.hazelcast.jet:hazelcast-enterprise-cdc-debezium:{full-version}' + implementation 'com.hazelcast.jet:hazelcast-enterprise-cdc-postgres:{full-version}' implementation 'com.fasterxml.jackson.core:jackson-annotations:2.11.0' } @@ -206,12 +230,12 @@ Maven:: com.hazelcast.jet - hazelcast-jet-cdc-debezium + hazelcast-enterprise-cdc-debezium {full-version} com.hazelcast.jet - hazelcast-jet-cdc-postgres + hazelcast-enterprise-cdc-postgres {full-version} @@ -241,6 +265,9 @@ Maven:: -- ==== +If you are using {open-source-product-name}, you have to replace `hazelcast-enterprise-cdc-debezium` +with `hazelcast-jet-cdc-debezium` and `hazelcast-enterprise-cdc-postgres` with `hazelcast-jet-cdc-postgres`. + == Step 6. Define Data Pipeline Let's write the code that will monitor the database and do something @@ -254,7 +281,55 @@ customer with a specific ID. This is how the code doing this looks like: -```java +[tabs] +==== +{enterprise-product-name}:: ++ +-- +[source,java] +---- +package org.example; + +import com.hazelcast.core.Hazelcast; +import com.hazelcast.core.HazelcastInstance; +import com.hazelcast.enterprise.jet.cdc.CdcSinks; +import com.hazelcast.enterprise.jet.cdc.ChangeRecord; +import com.hazelcast.enterprise.jet.cdc.postgres.PostgresCdcSources; +import com.hazelcast.jet.config.JobConfig; +import com.hazelcast.jet.pipeline.Pipeline; +import com.hazelcast.jet.pipeline.StreamSource; + +public class JetJob { + + public static void main(String[] args) { + StreamSource source = PostgresCdcSources.postgres("source") + .setDatabaseAddress("127.0.0.1", 5432) + .setDatabaseCredentials("postgres", "postgres") + .setDatabaseName("postgres") + .setTableIncludeList("inventory.customers") + .build(); + + Pipeline pipeline = Pipeline.create(); + pipeline.readFrom(source) + .withoutTimestamps() + .peek() + .writeTo(CdcSinks.map("customers", + r -> r.key().toMap().get("id"), + r -> r.value().toObject(Customer.class).toString())); + + JobConfig cfg = new JobConfig().setName("postgres-monitor"); + HazelcastInstance hz = Hazelcast.bootstrappedInstance(); + hz.getJet().newJob(pipeline, cfg); + } + +} +---- +-- +{open-source-product-name}:: ++ +-- +[source,java] +---- package org.example; import com.hazelcast.core.Hazelcast; @@ -292,11 +367,14 @@ public class JetJob { } } -``` +---- +-- +==== The `Customer` class we map change events to is quite simple too: -```java +[source,java] +---- package org.example; import com.fasterxml.jackson.annotation.JsonProperty; @@ -354,14 +432,15 @@ public class Customer implements Serializable { return "Customer {id=" + id + ", firstName=" + firstName + ", lastName=" + lastName + ", email=" + email + '}'; } } -``` +---- To make it evident that our pipeline serves the purpose of building an up-to-date cache of customers, which can be interrogated at any time let's add one more class. This code can be executed at any time in your IDE and will print the current content of the cache. -```java +[source,java] +---- package org.example; import com.hazelcast.client.HazelcastClient; @@ -379,7 +458,7 @@ public class CacheRead { } } -``` +---- == Step 7. Package the Pipeline into a JAR @@ -395,9 +474,10 @@ need to do is to run the build command: Gradle:: + -- -```bash +[source,bash] +---- gradle build -``` +---- This will produce a JAR file called `cdc-tutorial-1.0-SNAPSHOT.jar` in the `build/libs` directory of our project. @@ -405,9 +485,10 @@ in the `build/libs` directory of our project. Maven:: + -- -```bash +[source,bash] +---- mvn package -``` +---- This will produce a JAR file called `cdc-tutorial-1.0-SNAPSHOT.jar` in the `target` directory or our project. @@ -424,16 +505,18 @@ issue is following command: Gradle:: + -- -```bash +[source,bash] +---- bin/hz-cli submit build/libs/cdc-tutorial-1.0-SNAPSHOT.jar -``` +---- -- Maven:: + -- -```bash +[source,bash] +---- bin/hz-cli submit target/cdc-tutorial-1.0-SNAPSHOT.jar -``` +---- -- ==== @@ -441,7 +524,8 @@ The output in the Hazelcast member's log should look something like this (we also log what we put in the `IMap` sink thanks to the `peek()` stage we inserted): -``` +[source] +---- ... Snapshot ended with SnapshotResult [...] ... Obtained valid replication slot ReplicationSlot [...] ... REPLICA IDENTITY for 'inventory.customers' is 'FULL'; UPDATE AND DELETE events will contain the previous values of all the columns @@ -450,85 +534,95 @@ we inserted): ... Output to ordinal 0: key:{{"id":1003}}, value:{{"id":1003,"first_name":"Edward","last_name":"Walker",... ... Output to ordinal 0: key:{{"id":1004}}, value:{{"id":1004,"first_name":"Anne","last_name":"Kretchmar",... ... Transitioning from the snapshot reader to the binlog reader -``` +---- == Step 9. Track Updates Let's see how our cache looks like at this time. If we execute the `CacheRead` code <<6-define-jet-job, defined above>>, we'll get: -``` +[source] +---- Currently there are following customers in the cache: Customer {id=1002, firstName=George, lastName=Bailey, email=gbailey@foobar.com} Customer {id=1003, firstName=Edward, lastName=Walker, email=ed@walker.com} Customer {id=1004, firstName=Anne, lastName=Kretchmar, email=annek@noanswer.org} Customer {id=1001, firstName=Sally, lastName=Thomas, email=sally.thomas@acme.com} -``` +---- Let's do some updates in our database. Go to the PostgreSQL CLI <<3-start-postgresql-command-line-client, we've started earlier>> and run following update statement: -```bash +[source,bash] +---- UPDATE customers SET first_name='Anne Marie' WHERE id=1004; -``` +---- In the log of the Hazelcast member we should immediately see the effect: -``` +[source] +---- ... Output to ordinal 0: key:{{"id":1004}}, value:{{"id":1004,"first_name":"Anne Marie","last_name":"Kretchmar",... -``` +---- If we check the cache with `CacheRead` we get: -``` +[source] +---- Currently there are following customers in the cache: Customer {id=1002, firstName=George, lastName=Bailey, email=gbailey@foobar.com} Customer {id=1003, firstName=Edward, lastName=Walker, email=ed@walker.com} Customer {id=1004, firstName=Anne Marie, lastName=Kretchmar, email=annek@noanswer.org} Customer {id=1001, firstName=Sally, lastName=Thomas, email=sally.thomas@acme.com} -``` +---- One more: -```bash +[source,bash] +---- UPDATE customers SET email='edward.walker@walker.com' WHERE id=1003; -``` +---- -``` +[source] +---- Currently there are following customers in the cache: Customer {id=1002, firstName=George, lastName=Bailey, email=gbailey@foobar.com} Customer {id=1003, firstName=Edward, lastName=Walker, email=edward.walker@walker.com} Customer {id=1004, firstName=Anne Marie, lastName=Kretchmar, email=annek@noanswer.org} Customer {id=1001, firstName=Sally, lastName=Thomas, email=sally.thomas@acme.com} -``` +---- == Step 10. Clean Up . Cancel the job. + -```bash +[source,bash] +---- bin/hz-cli cancel postgres-monitor -``` +---- Shut down the Hazelcast cluster. + -```bash +[source,bash] +---- bin/hz-stop -``` +---- . Use Docker to stop the running container (this will kill the command-line client too, since it's running in the same container): + -```bash +[source,bash] +---- docker stop postgres -``` +---- + Since we've used the `--rm` flag when starting the connectors, Docker should remove them right after we stop them. We can verify that all processes are stopped and removed with following command: -```bash +[source,bash] +---- docker ps -a -``` +---- diff --git a/docs/modules/pipelines/pages/cdc.adoc b/docs/modules/pipelines/pages/cdc.adoc index a30649095..3645ca029 100644 --- a/docs/modules/pipelines/pages/cdc.adoc +++ b/docs/modules/pipelines/pages/cdc.adoc @@ -23,15 +23,16 @@ Open a terminal, and run following command. It will start a new container that runs a MySQL database server preconfigured with an inventory database: -```bash +[source,bash] +---- docker run -it --rm --name mysql -p 3306:3306 \ -e MYSQL_ROOT_PASSWORD=debezium -e MYSQL_USER=mysqluser \ - -e MYSQL_PASSWORD=mysqlpw debezium/example-mysql:1.2 -``` + -e MYSQL_PASSWORD=mysqlpw debezium/example-mysql:2.6 +---- -This runs a new container using version `1.2` of the +This runs a new container using version `2.6` of the link:https://hub.docker.com/r/debezium/example-mysql[debezium/example-mysql] -image (based on link:https://hub.docker.com/_/mysql[mysql:5.7]. It defines +image (based on link:https://hub.docker.com/_/mysql[mysql 8.2]. It defines and populates a sample "inventory" database and creates a `debezium` user with password `dbz` that has the minimum privileges required by Debezium’s MySQL connector. @@ -52,11 +53,12 @@ variables to specific values. You should see in your terminal something like the following: -```text +[source,text] +---- ... 2020-03-09T09:48:24.579480Z 0 [Note] mysqld: ready for connections. Version: '5.7.29-log' socket: '/var/run/mysqld/mysqld.sock' port: 3306 MySQL Community Server (GPL) -``` +---- Notice that the MySQL server starts and stops a few times as the configuration is modified. The last line listed above reports that the @@ -68,10 +70,11 @@ Open a new terminal, and use it to start a new container for the MySQL command line client and connect it to the MySQL server running in the `mysql` container: -```bash +[source,bash] +---- docker run -it --rm --name mysqlterm --link mysql --rm mysql:5.7 sh \ -c 'exec mysql -h"$MYSQL_PORT_3306_TCP_ADDR" -P"$MYSQL_PORT_3306_TCP_PORT" -uroot -p"$MYSQL_ENV_MYSQL_ROOT_PASSWORD"' -``` +---- Here we start the container using the `mysql:5.7` image, name the container `mysqlterm` and link it to the mysql container where the @@ -84,7 +87,8 @@ specifies the correct options so that it can connect properly. The container should output lines similar to the following: -``` +[source] +---- mysql: [Warning] Using a password on the command line interface can be insecure. Welcome to the MySQL monitor. Commands end with ; or \g. Your MySQL connection id is 4 @@ -99,25 +103,28 @@ owners. Type 'help;' or '\h' for help. Type '\c' to clear the current input statement. mysql> -``` +---- Unlike the other containers, this container runs a process that produces a prompt. We’ll use the prompt to interact with the database. First, switch to the "inventory" database: -```sql +[source,sql] +---- mysql> use inventory; -``` +---- and then list the tables in the database: -```sql +[source,sql] +---- mysql> show tables; -``` +---- which should then display: -``` +[source] +---- +---------------------+ | Tables_in_inventory | +---------------------+ @@ -129,14 +136,15 @@ which should then display: | products_on_hand | +---------------------+ 6 rows in set (0.01 sec) -``` +---- Use the MySQL command line client to explore the database and view the preloaded data. For example: -```sql +[source,sql] +---- mysql> SELECT * FROM customers; -``` +---- == Step 4. Start Hazelcast @@ -145,33 +153,36 @@ mysql> SELECT * FROM customers; If you already have Hazelcast and you skipped the above steps, make sure to follow from here on. -. Make sure the MySQL CDC plugin is in `lib/` directory. +. Make sure the MySQL CDC plugin is in the `lib/` directory. You must manually download the MySQL CDC plugin from link:https://repo1.maven.org/maven2/com/hazelcast/jet/hazelcast-enterprise-cdc-mysql/{full-version}/hazelcast-enterprise-cdc-mysql-{full-version}-jar-with-dependencies.jar[Hazelcast's Maven repository, window=_blank] and then copy it to the `lib/` directory. + -```bash +[source,bash] +---- ls lib/ -``` +---- + You should see the following jars: + -* hazelcast-jet-cdc-debezium-{full-version}.jar -* hazelcast-jet-cdc-mysql-{full-version}.jar -* hazelcast-jet-cdc-postgres-{full-version}.jar +* hazelcast-enterprise-cdc-debezium-{full-version}-jar-with-dependencies.jar +* hazelcast-enterprise-cdc-mysql-{full-version}-jar-with-dependencies.jar +* hazelcast-enterprise-cdc-postgres-{full-version}-jar-with-dependencies.jar + -WARNING: If you have Hazelcast {enterprise-product-name}, you need to manually download the MySQL CDC plugin from Hazelcast's Maven https://repo1.maven.org/maven2/com/hazelcast/jet/hazelcast-jet-cdc-mysql/{full-version}/hazelcast-jet-cdc-mysql-{full-version}-jar-with-dependencies.jar[repository] and then copy it to the `lib/` directory. +WARNING: If you have Hazelcast {enterprise-product-name}, you need to manually download the MySQL CDC plugin from https://repo1.maven.org/maven2/com/hazelcast/jet/hazelcast-jet-cdc-mysql/{full-version}/hazelcast-jet-cdc-mysql-{full-version}-jar-with-dependencies.jar[Hazelcast's Maven repository] and then copy it to the `lib/` directory. . Start Hazelcast. + -```bash +[source,bash] +---- bin/hz-start -``` +---- . When you see output like this, Hazelcast is up: + -``` +[source] +---- Members {size:1, ver:1} [ Member [192.168.1.5]:5701 - e7c26f7c-df9e-4994-a41d-203a1c63480e this ] -``` +---- == Step 5. Create a New Java Project @@ -197,8 +208,8 @@ repositories.mavenCentral() dependencies { implementation 'com.hazelcast:hazelcast:{full-version}' - implementation 'com.hazelcast.jet:hazelcast-jet-cdc-debezium:{full-version}' - implementation 'com.hazelcast.jet:hazelcast-jet-cdc-mysql:{full-version}' + implementation 'com.hazelcast.jet:hazelcast-enterprise-cdc-debezium:{full-version}' + implementation 'com.hazelcast.jet:hazelcast-enterprise-cdc-mysql:{full-version}' implementation 'com.fasterxml.jackson.core:jackson-annotations:2.11.0' } @@ -220,8 +231,7 @@ Maven:: 1.0-SNAPSHOT - 1.8 - 1.8 + 17 @@ -232,12 +242,12 @@ Maven:: com.hazelcast.jet - hazelcast-jet-cdc-debezium + hazelcast-enterprise-cdc-debezium {full-version} com.hazelcast.jet - hazelcast-jet-cdc-mysql + hazelcast-enterprise-cdc-mysql {full-version} @@ -280,14 +290,15 @@ customer with a specific ID. This is how the code doing this looks like: -```java +[source,java] +---- package org.example; import com.hazelcast.core.Hazelcast; import com.hazelcast.core.HazelcastInstance; -import com.hazelcast.jet.cdc.CdcSinks; -import com.hazelcast.jet.cdc.ChangeRecord; -import com.hazelcast.jet.cdc.mysql.MySqlCdcSources; +import com.hazelcast.enterprise.jet.cdc.CdcSinks; +import com.hazelcast.enterprise.jet.cdc.ChangeRecord; +import com.hazelcast.enterprise.jet.cdc.mysql.MySqlCdcSources; import com.hazelcast.jet.config.JobConfig; import com.hazelcast.jet.pipeline.Pipeline; import com.hazelcast.jet.pipeline.StreamSource; @@ -296,13 +307,11 @@ public class JetJob { public static void main(String[] args) { StreamSource source = MySqlCdcSources.mysql("source") - .setDatabaseAddress("127.0.0.1") - .setDatabasePort(3306) - .setDatabaseUser("debezium") - .setDatabasePassword("dbz") + .setDatabaseAddress("127.0.0.1", 3306) + .setDatabaseCredentials("debezium", "dbz") .setClusterName("dbserver1") - .setDatabaseWhitelist("inventory") - .setTableWhitelist("inventory.customers") + .setDatabaseIncludeList("inventory") + .setTableIncludeList("inventory.customers") .build(); Pipeline pipeline = Pipeline.create(); @@ -319,11 +328,14 @@ public class JetJob { } } -``` +---- + +NOTE: If you are using Hazelcast {open-source-product-name}, you have to change the package from `com.hazelcast.enterprise.jet...` to `com.hazelcast.jet...`. The `Customer` class we map change events to is quite simple too: -```java +[source,java] +---- package org.example; import com.fasterxml.jackson.annotation.JsonProperty; @@ -381,14 +393,15 @@ public class Customer implements Serializable { return "Customer {id=" + id + ", firstName=" + firstName + ", lastName=" + lastName + ", email=" + email + '}'; } } -``` +---- To make it evident that our pipeline serves the purpose of building an up-to-date cache of customers, which can be interrogated at any time let's add one more class. This code can be executed at any time in your IDE and will print the current content of the cache. -```java +[source,java] +---- package org.example; import com.hazelcast.client.HazelcastClient; @@ -406,7 +419,7 @@ public class CacheRead { } } -``` +---- == Step 7. Package the Pipeline into a JAR @@ -422,9 +435,10 @@ need to do is to run the build command: Gradle:: + -- -```bash +[source,bash] +---- gradle build -``` +---- This will produce a JAR file called `cdc-tutorial-1.0-SNAPSHOT.jar` in the `build/libs` directory of our project. @@ -433,9 +447,10 @@ Maven:: + -- -```bash +[source,bash] +---- mvn package -``` +---- This will produce a JAR file called `cdc-tutorial-1.0-SNAPSHOT.jar` in the `target` directory or our project. @@ -452,16 +467,18 @@ issue is following command: Gradle:: + -- -```bash +[source,bash] +---- bin/hz-cli submit build/libs/cdc-tutorial-1.0-SNAPSHOT.jar -``` +---- -- Maven:: + -- -```bash +[source,bash] +---- bin/hz-cli submit target/cdc-tutorial-1.0-SNAPSHOT.jar -``` +---- -- ==== @@ -469,95 +486,106 @@ The output in the Hazelcast member's log should look something like this (we also log what we put in the `IMap` sink thanks to the `peek()` stage we inserted): -``` +[source] +---- ... Completed snapshot in 00:00:01.519 ... Output to ordinal 0: key:{{"id":1001}}, value:{{"id":1001,"first_name":"Sally","last_name":"Thomas",... ... Output to ordinal 0: key:{{"id":1002}}, value:{{"id":1002,"first_name":"George","last_name":"Bailey",... ... Output to ordinal 0: key:{{"id":1003}}, value:{{"id":1003,"first_name":"Edward","last_name":"Walker",... ... Output to ordinal 0: key:{{"id":1004}}, value:{{"id":1004,"first_name":"Anne","last_name":"Kretchmar",... ... Transitioning from the snapshot reader to the binlog reader -``` +---- == Step 9. Track Updates Let's see how our cache looks like at this time. If we execute the `CacheRead` code <<6-define-jet-job, defined above>>, we'll get: -```text +[source,text] +---- Currently there are following customers in the cache: Customer {id=1002, firstName=George, lastName=Bailey, email=gbailey@foobar.com} Customer {id=1003, firstName=Edward, lastName=Walker, email=ed@walker.com} Customer {id=1004, firstName=Anne, lastName=Kretchmar, email=annek@noanswer.org} Customer {id=1001, firstName=Sally, lastName=Thomas, email=sally.thomas@acme.com} -``` +---- Let's do some updates in our database. Go to the MySQL CLI <> and run following update statement: -``` +[source,bash] +---- mysql> UPDATE customers SET first_name='Anne Marie' WHERE id=1004; Query OK, 1 row affected (0.00 sec) Rows matched: 1 Changed: 1 Warnings: 0 -``` +---- In the log of the Hazelcast member we should immediately see the effect: -``` +[source] +---- ... Output to ordinal 0: key:{{"id":1004}}, value:{{"id":1004,"first_name":"Anne Marie","last_name":"Kretchmar",... -``` +---- If we check the cache with `CacheRead` we get: -``` +[source] +---- Currently there are following customers in the cache: Customer {id=1002, firstName=George, lastName=Bailey, email=gbailey@foobar.com} Customer {id=1003, firstName=Edward, lastName=Walker, email=ed@walker.com} Customer {id=1004, firstName=Anne Marie, lastName=Kretchmar, email=annek@noanswer.org} Customer {id=1001, firstName=Sally, lastName=Thomas, email=sally.thomas@acme.com} -``` +---- One more: -``` +[source,bash] +---- mysql> UPDATE customers SET email='edward.walker@walker.com' WHERE id=1003; Query OK, 1 row affected (0.00 sec) Rows matched: 1 Changed: 1 Warnings: 0 -``` +---- -``` +[source] +---- Currently there are following customers in the cache: Customer {id=1002, firstName=George, lastName=Bailey, email=gbailey@foobar.com} Customer {id=1003, firstName=Edward, lastName=Walker, email=edward.walker@walker.com} Customer {id=1004, firstName=Anne Marie, lastName=Kretchmar, email=annek@noanswer.org} Customer {id=1001, firstName=Sally, lastName=Thomas, email=sally.thomas@acme.com} -``` +---- == Step 10. Clean up . Cancel the job. + -```bash +[source,bash] +---- bin/hz-cli cancel mysql-monitor -``` +---- + Shut down the Hazelcast cluster. + -```bash +[source,bash] +---- bin/hz-stop -``` +---- . Use Docker to stop the running container (this will kill the command-line client too, since it's running in the same container): + -```bash +[source,bash] +---- docker stop mysql -``` +---- + Since we've used the `--rm` flag when starting the connectors, Docker should remove them right after we stop them. We can verify that all processes are stopped and removed with following command: + -```bash +[source,bash] +---- docker ps -a -``` +---- From 0d936470ccdbb3d3ca399ca50d96aa4fad99304c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Burak=20G=C3=B6k?= Date: Wed, 9 Oct 2024 14:45:24 +0300 Subject: [PATCH 16/16] Clarify (lack of) backup redistribution on data structure level [HZG-156] (#1313) Adds an info-box to clarify the following sentence. > The distribution happens on the partition level; the data and its backups are stored in the memory partitions. --- docs/modules/fault-tolerance/pages/backups.adoc | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/docs/modules/fault-tolerance/pages/backups.adoc b/docs/modules/fault-tolerance/pages/backups.adoc index e693f20ba..e475afb06 100644 --- a/docs/modules/fault-tolerance/pages/backups.adoc +++ b/docs/modules/fault-tolerance/pages/backups.adoc @@ -7,6 +7,15 @@ The distribution happens on the partition level; the data and its backups are st memory partitions. See the xref:overview:data-partitioning.adoc[Data Partitioning] and xref:clusters:partition-group-configuration.adoc[Partition Grouping] for more information about the partitioning. +[IMPORTANT] +==== +Hazelcast does not support backup redistribution on a data structure level. Thus, changing the backup count of a nonempty data structure is **not** supported. For example, for a map: + +1. increasing the backup countfootnote:change-backup-count[The backup count of an empty map can only be changed by modifying the xref:configuration:understanding-configuration.adoc#static-configuration[static configuration] and restarting the cluster.] does **not** create additional copies of existing map entries, +2. decreasing the backup countfootnote:change-backup-count[] does **not** remove any backups of existing map entries, so some backups will have stale data if the map entries are overwritten, and +3. if some backups are lost due to node failure, additional copies are **not** created on other nodes to meet the backup count. +==== + When a member in your cluster is lost, Hazelcast redistributes the backups on the remaining members so that every partition has a backup. The number of backups is configurable.