From c862d18f07203f7f450f25ff6ca48246957c6eb4 Mon Sep 17 00:00:00 2001 From: Andrei Nadyktov Date: Fri, 30 Aug 2024 14:37:53 +0300 Subject: [PATCH 1/4] IGNITE-22530 CDC: Add regex filters for cache names --- .../ignite/cdc/AbstractIgniteCdcStreamer.java | 161 +++++++++++- .../ignite/cdc/IgniteToIgniteCdcStreamer.java | 6 +- ...VersionConflictResolverPluginProvider.java | 38 ++- .../AbstractKafkaToIgniteCdcStreamer.java | 10 +- .../cdc/kafka/IgniteToKafkaCdcStreamer.java | 161 +++++++++++- .../cdc/kafka/KafkaToIgniteCdcStreamer.java | 5 + .../KafkaToIgniteCdcStreamerApplier.java | 59 ++++- ...KafkaToIgniteCdcStreamerConfiguration.java | 37 +++ .../kafka/KafkaToIgniteClientCdcStreamer.java | 5 + .../thin/IgniteToIgniteClientCdcStreamer.java | 6 +- .../ignite/cdc/AbstractReplicationTest.java | 116 +++++++++ .../cdc/CdcIgniteToIgniteReplicationTest.java | 31 ++- .../apache/ignite/cdc/RegexFiltersTest.java | 233 ++++++++++++++++++ .../kafka/CdcKafkaReplicationAppsTest.java | 5 + .../cdc/kafka/CdcKafkaReplicationTest.java | 74 ++++-- .../KafkaToIgniteMetadataUpdaterTest.java | 2 +- 16 files changed, 910 insertions(+), 39 deletions(-) create mode 100644 modules/cdc-ext/src/test/java/org/apache/ignite/cdc/RegexFiltersTest.java diff --git a/modules/cdc-ext/src/main/java/org/apache/ignite/cdc/AbstractIgniteCdcStreamer.java b/modules/cdc-ext/src/main/java/org/apache/ignite/cdc/AbstractIgniteCdcStreamer.java index f56a9954e..a2f830f28 100644 --- a/modules/cdc-ext/src/main/java/org/apache/ignite/cdc/AbstractIgniteCdcStreamer.java +++ b/modules/cdc-ext/src/main/java/org/apache/ignite/cdc/AbstractIgniteCdcStreamer.java @@ -17,9 +17,19 @@ package org.apache.ignite.cdc; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardOpenOption; +import java.util.Collections; +import java.util.HashSet; import java.util.Iterator; +import java.util.List; import java.util.Set; +import java.util.regex.Pattern; +import java.util.regex.PatternSyntaxException; import java.util.stream.Collectors; + import org.apache.ignite.IgniteCheckedException; import org.apache.ignite.IgniteException; import org.apache.ignite.IgniteLogger; @@ -67,12 +77,30 @@ public abstract class AbstractIgniteCdcStreamer implements CdcConsumer { /** */ public static final String LAST_EVT_SENT_TIME_DESC = "Timestamp of last applied event to destination cluster"; + /** File with saved names of caches added by cache masks. */ + private static final String SAVED_CACHES_FILE = "caches"; + + /** CDC directory path. */ + private Path cdcDir; + /** Handle only primary entry flag. */ private boolean onlyPrimary = DFLT_IS_ONLY_PRIMARY; /** Cache names. */ private Set caches; + /** Include regex templates for cache names. */ + private Set includeTemplates = new HashSet<>(); + + /** Compiled include regex patterns for cache names. */ + private Set includeFilters; + + /** Exclude regex templates for cache names. */ + private Set excludeTemplates = new HashSet<>(); + + /** Compiled exclude regex patterns for cache names. */ + private Set excludeFilters; + /** Cache IDs. */ protected Set cachesIds; @@ -99,14 +127,28 @@ public abstract class AbstractIgniteCdcStreamer implements CdcConsumer { protected IgniteLogger log; /** {@inheritDoc} */ - @Override public void start(MetricRegistry reg) { + @Override public void start(MetricRegistry reg, Path cdcDir) { A.notEmpty(caches, "caches"); + this.cdcDir = cdcDir; + cachesIds = caches.stream() .mapToInt(CU::cacheId) .boxed() .collect(Collectors.toSet()); + prepareRegexFilters(); + + try { + loadCaches().stream() + .filter(this::matchesFilters) + .map(CU::cacheId) + .forEach(cachesIds::add); + } + catch (IOException e) { + throw new IgniteException(e); + } + MetricRegistryImpl mreg = (MetricRegistryImpl)reg; this.evtsCnt = mreg.longMetric(EVTS_SENT_CNT, EVTS_SENT_CNT_DESC); @@ -144,10 +186,101 @@ public abstract class AbstractIgniteCdcStreamer implements CdcConsumer { /** {@inheritDoc} */ @Override public void onCacheChange(Iterator cacheEvents) { cacheEvents.forEachRemaining(e -> { - // Just skip. Handle of cache events not supported. + matchWithRegexTemplates(e.configuration().getName()); }); } + /** + * Finds match between cache name and user's regex templates. + * If match found, adds this cache's id to id's list and saves cache name to file. + * + * @param cacheName Cache name. + */ + private void matchWithRegexTemplates(String cacheName) { + int cacheId = CU.cacheId(cacheName); + + if (!cachesIds.contains(cacheId) && matchesFilters(cacheName)) { + cachesIds.add(cacheId); + + try { + saveCache(cacheName); + } + catch (IOException e) { + throw new IgniteException(e); + } + + if (log.isInfoEnabled()) + log.info("Cache has been added to replication [cacheName=" + cacheName + "]"); + } + } + + /** + * Writes cache name to file + * + * @param cacheName Cache name. + */ + private void saveCache(String cacheName) throws IOException { + if (cdcDir != null) { + Path savedCachesPath = cdcDir.resolve(SAVED_CACHES_FILE); + + String cn = cacheName + '\n'; + + Files.write(savedCachesPath, cn.getBytes(), StandardOpenOption.APPEND); + } + } + + /** + * Loads saved caches from file. + * + * @return List of saved caches names. + */ + private List loadCaches() throws IOException { + if (cdcDir != null) { + Path savedCachesPath = cdcDir.resolve(SAVED_CACHES_FILE); + + if (Files.notExists(savedCachesPath)) { + Files.createFile(savedCachesPath); + + if (log.isInfoEnabled()) + log.info("Cache list created: " + savedCachesPath); + } + + return Files.readAllLines(savedCachesPath); + } + return Collections.emptyList(); + } + + /** + * Compiles regex patterns from user templates. + * + * @throws PatternSyntaxException If the template's syntax is invalid + */ + private void prepareRegexFilters() { + includeFilters = includeTemplates.stream() + .map(Pattern::compile) + .collect(Collectors.toSet()); + + excludeFilters = excludeTemplates.stream() + .map(Pattern::compile) + .collect(Collectors.toSet()); + } + + /** + * Matches cache name with compiled regex patterns. + * + * @param cacheName Cache name. + * @return True if cache name match include patterns and don't match exclude patterns. + */ + private boolean matchesFilters(String cacheName) { + boolean matchesInclude = includeFilters.stream() + .anyMatch(pattern -> pattern.matcher(cacheName).matches()); + + boolean notMatchesExclude = excludeFilters.stream() + .noneMatch(pattern -> pattern.matcher(cacheName).matches()); + + return matchesInclude && notMatchesExclude; + } + /** {@inheritDoc} */ @Override public void onCacheDestroy(Iterator caches) { caches.forEachRemaining(e -> { @@ -238,6 +371,30 @@ public AbstractIgniteCdcStreamer setCaches(Set caches) { return this; } + /** + * Sets include regex patterns that participate in CDC. + * + * @param includeTemplates Include regex templates + * @return {@code this} for chaining. + */ + public AbstractIgniteCdcStreamer setIncludeTemplates(Set includeTemplates) { + this.includeTemplates = includeTemplates; + + return this; + } + + /** + * Sets exclude regex patterns that participate in CDC. + * + * @param excludeTemplates Exclude regex templates + * @return {@code this} for chaining. + */ + public AbstractIgniteCdcStreamer setExcludeTemplates(Set excludeTemplates) { + this.excludeTemplates = excludeTemplates; + + return this; + } + /** * Sets maximum batch size that will be applied to destination cluster. * diff --git a/modules/cdc-ext/src/main/java/org/apache/ignite/cdc/IgniteToIgniteCdcStreamer.java b/modules/cdc-ext/src/main/java/org/apache/ignite/cdc/IgniteToIgniteCdcStreamer.java index 618c61d70..902b8ca6b 100644 --- a/modules/cdc-ext/src/main/java/org/apache/ignite/cdc/IgniteToIgniteCdcStreamer.java +++ b/modules/cdc-ext/src/main/java/org/apache/ignite/cdc/IgniteToIgniteCdcStreamer.java @@ -17,6 +17,8 @@ package org.apache.ignite.cdc; +import java.nio.file.Path; + import org.apache.ignite.IgniteException; import org.apache.ignite.Ignition; import org.apache.ignite.cdc.conflictresolve.CacheVersionConflictResolverImpl; @@ -61,8 +63,8 @@ public class IgniteToIgniteCdcStreamer extends AbstractIgniteCdcStreamer impleme private volatile boolean alive = true; /** {@inheritDoc} */ - @Override public void start(MetricRegistry mreg) { - super.start(mreg); + @Override public void start(MetricRegistry mreg, Path cdcDir) { + super.start(mreg, cdcDir); if (log.isInfoEnabled()) log.info("Ignite To Ignite Streamer [cacheIds=" + cachesIds + ']'); diff --git a/modules/cdc-ext/src/main/java/org/apache/ignite/cdc/conflictresolve/CacheVersionConflictResolverPluginProvider.java b/modules/cdc-ext/src/main/java/org/apache/ignite/cdc/conflictresolve/CacheVersionConflictResolverPluginProvider.java index 0083f136a..f283b6c2c 100644 --- a/modules/cdc-ext/src/main/java/org/apache/ignite/cdc/conflictresolve/CacheVersionConflictResolverPluginProvider.java +++ b/modules/cdc-ext/src/main/java/org/apache/ignite/cdc/conflictresolve/CacheVersionConflictResolverPluginProvider.java @@ -18,8 +18,11 @@ package org.apache.ignite.cdc.conflictresolve; import java.io.Serializable; +import java.util.HashSet; import java.util.Set; import java.util.UUID; +import java.util.regex.Pattern; + import org.apache.ignite.IgniteLogger; import org.apache.ignite.cluster.ClusterNode; import org.apache.ignite.internal.IgniteEx; @@ -65,6 +68,12 @@ public class CacheVersionConflictResolverPluginProvider includeTemplates = new HashSet<>(); + + /** Exclude regex templates for cache names. */ + private Set excludeTemplates = new HashSet<>(); + /** Log. */ private IgniteLogger log; @@ -98,7 +107,7 @@ public CacheVersionConflictResolverPluginProvider() { @Override public CachePluginProvider createCacheProvider(CachePluginContext ctx) { String cacheName = ctx.igniteCacheConfiguration().getName(); - if (caches.contains(cacheName)) { + if (caches.contains(cacheName) || matchesFilters(cacheName)) { log.info("ConflictResolver provider set for cache [cacheName=" + cacheName + ']'); return provider; @@ -144,6 +153,16 @@ public void setConflictResolver(CacheVersionConflictResolver resolver) { this.resolver = resolver; } + /** @param includeTemplates Include regex templates */ + public void setIncludeTemplates(Set includeTemplates) { + this.includeTemplates = includeTemplates; + } + + /** @param excludeTemplates Exclude regex templates */ + public void setExcludeTemplates(Set excludeTemplates) { + this.excludeTemplates = excludeTemplates; + } + /** {@inheritDoc} */ @Override public void start(PluginContext ctx) { ((IgniteEx)ctx.grid()).context().cache().context().versions().dataCenterId(clusterId); @@ -178,4 +197,21 @@ public void setConflictResolver(CacheVersionConflictResolver resolver) { @Nullable @Override public T createComponent(PluginContext ctx, Class cls) { return null; } + + /** + * Match cache name with regex patterns. + * + * @param cacheName Cache name. + */ + private boolean matchesFilters(String cacheName) { + boolean matchesInclude = includeTemplates.stream() + .map(Pattern::compile) + .anyMatch(pattern -> pattern.matcher(cacheName).matches()); + + boolean notMatchesExclude = excludeTemplates.stream() + .map(Pattern::compile) + .noneMatch(pattern -> pattern.matcher(cacheName).matches()); + + return matchesInclude && notMatchesExclude; + } } diff --git a/modules/cdc-ext/src/main/java/org/apache/ignite/cdc/kafka/AbstractKafkaToIgniteCdcStreamer.java b/modules/cdc-ext/src/main/java/org/apache/ignite/cdc/kafka/AbstractKafkaToIgniteCdcStreamer.java index 5332f1bd1..da01403d6 100644 --- a/modules/cdc-ext/src/main/java/org/apache/ignite/cdc/kafka/AbstractKafkaToIgniteCdcStreamer.java +++ b/modules/cdc-ext/src/main/java/org/apache/ignite/cdc/kafka/AbstractKafkaToIgniteCdcStreamer.java @@ -181,7 +181,8 @@ protected void runAppliers() { caches, metaUpdr, stopped, - metrics + metrics, + this ); addAndStart("applier-thread-" + cntr++, applier); @@ -252,6 +253,13 @@ private void addAndStart(String threadName, /** Checks that configured caches exist in a destination cluster. */ protected abstract void checkCaches(Collection caches); + /** + * Get cache names from client. + * + * @return Cache names. + * */ + protected abstract Collection getCaches(); + /** */ private void ackAsciiLogo(IgniteLogger log) { String ver = "ver. " + ACK_VER_STR; diff --git a/modules/cdc-ext/src/main/java/org/apache/ignite/cdc/kafka/IgniteToKafkaCdcStreamer.java b/modules/cdc-ext/src/main/java/org/apache/ignite/cdc/kafka/IgniteToKafkaCdcStreamer.java index db1ffa49f..9d60cbe0f 100644 --- a/modules/cdc-ext/src/main/java/org/apache/ignite/cdc/kafka/IgniteToKafkaCdcStreamer.java +++ b/modules/cdc-ext/src/main/java/org/apache/ignite/cdc/kafka/IgniteToKafkaCdcStreamer.java @@ -17,8 +17,14 @@ package org.apache.ignite.cdc.kafka; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardOpenOption; import java.util.ArrayList; import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Properties; @@ -28,8 +34,12 @@ import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; import java.util.function.Function; +import java.util.regex.Pattern; +import java.util.regex.PatternSyntaxException; import java.util.stream.Collectors; import java.util.stream.IntStream; + +import org.apache.ignite.IgniteException; import org.apache.ignite.IgniteLogger; import org.apache.ignite.binary.BinaryType; import org.apache.ignite.cdc.CdcCacheEvent; @@ -149,6 +159,24 @@ public class IgniteToKafkaCdcStreamer implements CdcConsumer { /** Cache names. */ private Collection caches; + /** File with saved names of caches added by cache masks. */ + private static final String SAVED_CACHES_FILE = "caches"; + + /** CDC directory path. */ + private Path cdcDir; + + /** Include regex templates for cache names. */ + private Set includeTemplates = new HashSet<>(); + + /** Compiled include regex patterns for cache names. */ + private Set includeFilters; + + /** Exclude regex templates for cache names. */ + private Set excludeTemplates = new HashSet<>(); + + /** Compiled exclude regex patterns for cache names. */ + private Set excludeFilters; + /** Max batch size. */ private int maxBatchSz = DFLT_MAX_BATCH_SIZE; @@ -248,7 +276,7 @@ public class IgniteToKafkaCdcStreamer implements CdcConsumer { /** {@inheritDoc} */ @Override public void onCacheChange(Iterator cacheEvents) { cacheEvents.forEachRemaining(e -> { - // Just skip. Handle of cache events not supported. + matchWithRegexTemplates(e.configuration().getName()); }); } @@ -320,7 +348,7 @@ private void sendOneBatch( } /** {@inheritDoc} */ - @Override public void start(MetricRegistry reg) { + @Override public void start(MetricRegistry reg, Path cdcDir) { A.notNull(kafkaProps, "Kafka properties"); A.notNull(evtTopic, "Kafka topic"); A.notNull(metadataTopic, "Kafka metadata topic"); @@ -331,10 +359,24 @@ private void sendOneBatch( kafkaProps.setProperty(KEY_SERIALIZER_CLASS_CONFIG, IntegerSerializer.class.getName()); kafkaProps.setProperty(VALUE_SERIALIZER_CLASS_CONFIG, ByteArraySerializer.class.getName()); + this.cdcDir = cdcDir; + cachesIds = caches.stream() .map(CU::cacheId) .collect(Collectors.toSet()); + prepareRegexFilters(); + + try { + loadCaches().stream() + .filter(this::matchesFilters) + .map(CU::cacheId) + .forEach(cachesIds::add); + } + catch (IOException e) { + throw new IgniteException(e); + } + try { producer = new KafkaProducer<>(kafkaProps); @@ -380,6 +422,97 @@ public IgniteToKafkaCdcStreamer setOnlyPrimary(boolean onlyPrimary) { return this; } + /** + * Compiles regex patterns from user templates. + * + * @throws PatternSyntaxException If the template's syntax is invalid + */ + private void prepareRegexFilters() { + includeFilters = includeTemplates.stream() + .map(Pattern::compile) + .collect(Collectors.toSet()); + + excludeFilters = excludeTemplates.stream() + .map(Pattern::compile) + .collect(Collectors.toSet()); + } + + /** + * Loads saved caches from file. + * + * @return List of saved caches names. + */ + private List loadCaches() throws IOException { + if (cdcDir != null) { + Path savedCachesPath = cdcDir.resolve(SAVED_CACHES_FILE); + + if (Files.notExists(savedCachesPath)) { + Files.createFile(savedCachesPath); + + if (log.isInfoEnabled()) + log.info("Cache list created: " + savedCachesPath); + } + + return Files.readAllLines(savedCachesPath); + } + return Collections.emptyList(); + } + + /** + * Matches cache name with compiled regex patterns. + * + * @param cacheName Cache name. + * @return True if cache name match include patterns and don't match exclude patterns. + */ + private boolean matchesFilters(String cacheName) { + boolean matchesInclude = includeFilters.stream() + .anyMatch(pattern -> pattern.matcher(cacheName).matches()); + + boolean notMatchesExclude = excludeFilters.stream() + .noneMatch(pattern -> pattern.matcher(cacheName).matches()); + + return matchesInclude && notMatchesExclude; + } + + /** + * Finds match between cache name and user's regex templates. + * If match found, adds this cache's id to id's list and saves cache name to file. + * + * @param cacheName Cache name. + */ + private void matchWithRegexTemplates(String cacheName) { + int cacheId = CU.cacheId(cacheName); + + if (!cachesIds.contains(cacheId) && matchesFilters(cacheName)) { + cachesIds.add(cacheId); + + try { + saveCache(cacheName); + } + catch (IOException e) { + throw new IgniteException(e); + } + + if (log.isInfoEnabled()) + log.info("Cache has been added to replication [cacheName=" + cacheName + "]"); + } + } + + /** + * Writes cache name to file. + * + * @param cacheName Cache name. + */ + private void saveCache(String cacheName) throws IOException { + if (cdcDir != null) { + Path savedCaches = cdcDir.resolve(SAVED_CACHES_FILE); + + String cn = cacheName + '\n'; + + Files.write(savedCaches, cn.getBytes(), StandardOpenOption.APPEND); + } + } + /** * Sets topic that is used to send data to Kafka. * @@ -428,6 +561,30 @@ public IgniteToKafkaCdcStreamer setCaches(Collection caches) { return this; } + /** + * Sets include regex patterns that participate in CDC. + * + * @param includeTemplates Include regex templates. + * @return {@code this} for chaining. + */ + public IgniteToKafkaCdcStreamer setIncludeTemplates(Set includeTemplates) { + this.includeTemplates = includeTemplates; + + return this; + } + + /** + * Sets exclude regex patterns that participate in CDC. + * + * @param excludeTemplates Exclude regex templates + * @return {@code this} for chaining. + */ + public IgniteToKafkaCdcStreamer setExcludeTemplates(Set excludeTemplates) { + this.excludeTemplates = excludeTemplates; + + return this; + } + /** * Sets maximum batch size. * diff --git a/modules/cdc-ext/src/main/java/org/apache/ignite/cdc/kafka/KafkaToIgniteCdcStreamer.java b/modules/cdc-ext/src/main/java/org/apache/ignite/cdc/kafka/KafkaToIgniteCdcStreamer.java index 3386a4c01..75f4f205b 100644 --- a/modules/cdc-ext/src/main/java/org/apache/ignite/cdc/kafka/KafkaToIgniteCdcStreamer.java +++ b/modules/cdc-ext/src/main/java/org/apache/ignite/cdc/kafka/KafkaToIgniteCdcStreamer.java @@ -127,4 +127,9 @@ public KafkaToIgniteCdcStreamer( @Override protected void checkCaches(Collection caches) { caches.forEach(name -> Objects.requireNonNull(ign.cache(name), name + " not exists!")); } + + /** {@inheritDoc} */ + @Override protected Collection getCaches() { + return ign.cacheNames(); + } } diff --git a/modules/cdc-ext/src/main/java/org/apache/ignite/cdc/kafka/KafkaToIgniteCdcStreamerApplier.java b/modules/cdc-ext/src/main/java/org/apache/ignite/cdc/kafka/KafkaToIgniteCdcStreamerApplier.java index c28e18ba9..4b67189c4 100644 --- a/modules/cdc-ext/src/main/java/org/apache/ignite/cdc/kafka/KafkaToIgniteCdcStreamerApplier.java +++ b/modules/cdc-ext/src/main/java/org/apache/ignite/cdc/kafka/KafkaToIgniteCdcStreamerApplier.java @@ -27,11 +27,14 @@ import java.util.Iterator; import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.Properties; import java.util.Set; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicLong; import java.util.function.Supplier; +import java.util.regex.Pattern; + import org.apache.ignite.IgniteCheckedException; import org.apache.ignite.IgniteException; import org.apache.ignite.IgniteLogger; @@ -42,6 +45,7 @@ import org.apache.ignite.internal.processors.cache.version.CacheVersionConflictResolver; import org.apache.ignite.internal.processors.cache.version.GridCacheVersion; import org.apache.ignite.internal.util.typedef.F; +import org.apache.ignite.internal.util.typedef.internal.CU; import org.apache.ignite.internal.util.typedef.internal.S; import org.apache.kafka.clients.consumer.ConsumerRecord; import org.apache.kafka.clients.consumer.ConsumerRecords; @@ -104,6 +108,12 @@ class KafkaToIgniteCdcStreamerApplier implements Runnable, AutoCloseable { /** Caches ids to read. */ private final Set caches; + /** Include regex templates for cache names. */ + private final Set includeTemplates; + + /** Exclude regex templates for cache names. */ + private final Set excludeTemplates; + /** The maximum time to complete Kafka related requests, in milliseconds. */ private final long kafkaReqTimeout; @@ -128,6 +138,9 @@ class KafkaToIgniteCdcStreamerApplier implements Runnable, AutoCloseable { /** CDC kafka to ignite metrics */ private final KafkaToIgniteMetrics metrics; + /** Instance of KafkaToIgniteCdcStreamer */ + private final AbstractKafkaToIgniteCdcStreamer streamer; + /** * @param applierSupplier Cdc events applier supplier. * @param log Logger. @@ -139,6 +152,7 @@ class KafkaToIgniteCdcStreamerApplier implements Runnable, AutoCloseable { * @param metaUpdr Metadata updater. * @param stopped Stopped flag. * @param metrics CDC Kafka to Ignite metrics. + * @param streamer Instance of KafkaToIgniteCdcStreamer */ public KafkaToIgniteCdcStreamerApplier( Supplier applierSupplier, @@ -150,7 +164,8 @@ public KafkaToIgniteCdcStreamerApplier( Set caches, KafkaToIgniteMetadataUpdater metaUpdr, AtomicBoolean stopped, - KafkaToIgniteMetrics metrics + KafkaToIgniteMetrics metrics, + AbstractKafkaToIgniteCdcStreamer streamer ) { this.applierSupplier = applierSupplier; this.kafkaProps = kafkaProps; @@ -164,6 +179,9 @@ public KafkaToIgniteCdcStreamerApplier( this.stopped = stopped; this.log = log.getLogger(KafkaToIgniteCdcStreamerApplier.class); this.metrics = metrics; + this.streamer = streamer; + this.includeTemplates = streamerCfg.getIncludeTemplates(); + this.excludeTemplates = streamerCfg.getExcludeTemplates(); } /** {@inheritDoc} */ @@ -260,7 +278,44 @@ private boolean filterAndPossiblyUpdateMetadata(ConsumerRecord metrics.incrementReceivedEvents(); - return F.isEmpty(caches) || caches.contains(rec.key()); + return F.isEmpty(caches) || caches.contains(rec.key()) || matchesRegexTemplates(rec.key()); + } + + /** + * Gets caches names from CDC client and finds match + * between cache id and user's regex templates. + * + * @param key Cache id. + * @return True if match is found. + */ + private boolean matchesRegexTemplates(Integer key) { + Optional cache = streamer.getCaches().stream() + .filter(name -> CU.cacheId(name) == key) + .findAny(); + + Optional matchedCache = cache.filter(this::matchesFilters); + + matchedCache.ifPresent(c -> caches.add(CU.cacheId(c))); + + return matchedCache.isPresent(); + } + + /** + * Matches cache name with compiled regex patterns. + * + * @param cacheName Cache name. + * @return True if cache name match include patterns and don't match exclude patterns. + */ + private boolean matchesFilters(String cacheName) { + boolean matchesInclude = includeTemplates.stream() + .map(Pattern::compile) + .anyMatch(pattern -> pattern.matcher(cacheName).matches()); + + boolean notMatchesExclude = excludeTemplates.stream() + .map(Pattern::compile) + .noneMatch(pattern -> pattern.matcher(cacheName).matches()); + + return matchesInclude && notMatchesExclude; } /** diff --git a/modules/cdc-ext/src/main/java/org/apache/ignite/cdc/kafka/KafkaToIgniteCdcStreamerConfiguration.java b/modules/cdc-ext/src/main/java/org/apache/ignite/cdc/kafka/KafkaToIgniteCdcStreamerConfiguration.java index 07b97e2b0..9876037ec 100644 --- a/modules/cdc-ext/src/main/java/org/apache/ignite/cdc/kafka/KafkaToIgniteCdcStreamerConfiguration.java +++ b/modules/cdc-ext/src/main/java/org/apache/ignite/cdc/kafka/KafkaToIgniteCdcStreamerConfiguration.java @@ -18,7 +18,10 @@ package org.apache.ignite.cdc.kafka; import java.util.Collection; +import java.util.HashSet; import java.util.Map; +import java.util.Set; + import org.apache.ignite.cdc.CdcConfiguration; import org.apache.ignite.internal.processors.cache.IgniteInternalCache; import org.apache.ignite.lang.IgniteExperimental; @@ -85,6 +88,12 @@ public class KafkaToIgniteCdcStreamerConfiguration { */ private Collection caches; + /** Include regex templates for cache names. */ + private Set includeTemplates = new HashSet<>(); + + /** Exclude regex templates for cache names. */ + private Set excludeTemplates = new HashSet<>(); + /** Metric exporter SPI. */ private MetricExporterSpi[] metricExporterSpi; @@ -175,6 +184,34 @@ public void setCaches(Collection caches) { this.caches = caches; } + /** + * @return Include regex templates + */ + public Set getIncludeTemplates() { + return includeTemplates; + } + + /** + * @param includeTemplates Include regex templates + */ + public void setIncludeTemplates(Set includeTemplates) { + this.includeTemplates = includeTemplates; + } + + /** + * @return Exclude regex templates + */ + public Set getExcludeTemplates() { + return excludeTemplates; + } + + /** + * @param excludeTemplates Exclude regex templates + */ + public void setExcludeTemplates(Set excludeTemplates) { + this.excludeTemplates = excludeTemplates; + } + /** @return The maximum time to complete Kafka related requests, in milliseconds. */ public long getKafkaRequestTimeout() { return kafkaReqTimeout; diff --git a/modules/cdc-ext/src/main/java/org/apache/ignite/cdc/kafka/KafkaToIgniteClientCdcStreamer.java b/modules/cdc-ext/src/main/java/org/apache/ignite/cdc/kafka/KafkaToIgniteClientCdcStreamer.java index 6d05aab75..5c85558eb 100644 --- a/modules/cdc-ext/src/main/java/org/apache/ignite/cdc/kafka/KafkaToIgniteClientCdcStreamer.java +++ b/modules/cdc-ext/src/main/java/org/apache/ignite/cdc/kafka/KafkaToIgniteClientCdcStreamer.java @@ -127,4 +127,9 @@ public KafkaToIgniteClientCdcStreamer( caches.forEach(name -> A.ensure(clusterCaches.contains(name), name + " not exists!")); } + + /** {@inheritDoc} */ + @Override protected Collection getCaches() { + return client.cacheNames(); + } } diff --git a/modules/cdc-ext/src/main/java/org/apache/ignite/cdc/thin/IgniteToIgniteClientCdcStreamer.java b/modules/cdc-ext/src/main/java/org/apache/ignite/cdc/thin/IgniteToIgniteClientCdcStreamer.java index bc7af745b..607ca19c6 100644 --- a/modules/cdc-ext/src/main/java/org/apache/ignite/cdc/thin/IgniteToIgniteClientCdcStreamer.java +++ b/modules/cdc-ext/src/main/java/org/apache/ignite/cdc/thin/IgniteToIgniteClientCdcStreamer.java @@ -17,6 +17,8 @@ package org.apache.ignite.cdc.thin; +import java.nio.file.Path; + import org.apache.ignite.Ignition; import org.apache.ignite.cdc.AbstractIgniteCdcStreamer; import org.apache.ignite.cdc.conflictresolve.CacheVersionConflictResolverImpl; @@ -66,8 +68,8 @@ public class IgniteToIgniteClientCdcStreamer extends AbstractIgniteCdcStreamer { private long aliveCheckTimeout = DFLT_ALIVE_CHECK_TIMEOUT; /** {@inheritDoc} */ - @Override public void start(MetricRegistry mreg) { - super.start(mreg); + @Override public void start(MetricRegistry mreg, Path cdcDir) { + super.start(mreg, cdcDir); if (log.isInfoEnabled()) log.info("Ignite To Ignite Client Streamer [cacheIds=" + cachesIds + ']'); diff --git a/modules/cdc-ext/src/test/java/org/apache/ignite/cdc/AbstractReplicationTest.java b/modules/cdc-ext/src/test/java/org/apache/ignite/cdc/AbstractReplicationTest.java index 9a5e18c7f..c79657f76 100644 --- a/modules/cdc-ext/src/test/java/org/apache/ignite/cdc/AbstractReplicationTest.java +++ b/modules/cdc-ext/src/test/java/org/apache/ignite/cdc/AbstractReplicationTest.java @@ -25,6 +25,7 @@ import java.util.EnumSet; import java.util.HashSet; import java.util.List; +import java.util.Set; import java.util.concurrent.ThreadLocalRandom; import java.util.concurrent.TimeUnit; import java.util.function.BiConsumer; @@ -146,6 +147,18 @@ public static Collection parameters() { /** */ public static final String IGNORED_CACHE = "ignored-cache"; + /** */ + public static final String REGEX_INCLUDE_TEMPLATE_CACHE = "cdc_on_cache"; + + /** */ + public static final String REGEX_EXCLUDE_TEMPLATE_CACHE = "cdc_on_excluded_cache"; + + /** */ + public static final String REGEX_INCLUDE_PATTERN = "cdc_on.*"; + + /** */ + public static final String REGEX_EXCLUDE_PATTERN = "cdc_on_excluded.*"; + /** */ public static final byte SRC_CLUSTER_ID = 1; @@ -200,6 +213,8 @@ private enum WaitDataMode { cfgPlugin1.setClusterId(clusterId); cfgPlugin1.setCaches(new HashSet<>(Arrays.asList(ACTIVE_PASSIVE_CACHE, ACTIVE_ACTIVE_CACHE))); + cfgPlugin1.setIncludeTemplates(new HashSet<>(Arrays.asList(REGEX_INCLUDE_PATTERN))); + cfgPlugin1.setExcludeTemplates(new HashSet<>(Arrays.asList(REGEX_EXCLUDE_PATTERN))); cfgPlugin1.setConflictResolveField("reqId"); cfg.setPluginProviders(cfgPlugin1); @@ -562,6 +577,98 @@ public void testWithExpiryPolicy() throws Exception { } } + /** Check that caches matching regex filters in config, are added to CDC after its creation. + * Active/Active mode means changes made in both clusters. */ + @Test + public void testActiveActiveReplicationWithRegexFilters() throws Exception { + Set includeTemplates = new HashSet<>(Arrays.asList(REGEX_INCLUDE_PATTERN)); + Set excludeTemplates = new HashSet<>(Arrays.asList(REGEX_EXCLUDE_PATTERN)); + + createCache(srcCluster[0], ACTIVE_ACTIVE_CACHE); + createCache(destCluster[0], ACTIVE_ACTIVE_CACHE); + + IgniteCache srcCache = createCache(srcCluster[0], REGEX_INCLUDE_TEMPLATE_CACHE); + IgniteCache destCache = createCache(destCluster[0], REGEX_INCLUDE_TEMPLATE_CACHE); + + // Even keys goes to src cluster. + runAsync(generateData(REGEX_INCLUDE_TEMPLATE_CACHE, srcCluster[srcCluster.length - 1], + IntStream.range(0, KEYS_CNT).filter(i -> i % 2 == 0))); + + // Odd keys goes to dest cluster. + runAsync(generateData(REGEX_INCLUDE_TEMPLATE_CACHE, destCluster[destCluster.length - 1], + IntStream.range(0, KEYS_CNT).filter(i -> i % 2 != 0))); + + //Start CDC with only 'active-active-cache' in 'caches' property of CDC config + List> futs = startActiveActiveCdcWithFilters(includeTemplates, excludeTemplates); + + try { + waitForSameData(srcCache, destCache, KEYS_CNT, WaitDataMode.EXISTS, futs); + + runAsync(() -> IntStream.range(0, KEYS_CNT).filter(j -> j % 2 == 0).forEach(srcCache::remove)); + runAsync(() -> IntStream.range(0, KEYS_CNT).filter(j -> j % 2 != 0).forEach(destCache::remove)); + + waitForSameData(srcCache, destCache, KEYS_CNT, WaitDataMode.REMOVED, futs); + + //Shouldn't add to the replication, otherwise CDC will throw an error + runAsync(generateData(REGEX_EXCLUDE_TEMPLATE_CACHE, srcCluster[srcCluster.length - 1], IntStream.range(0, KEYS_CNT))); + + assertFalse(destCluster[0].cacheNames().contains(REGEX_EXCLUDE_TEMPLATE_CACHE)); + } + finally { + for (IgniteInternalFuture fut : futs) + fut.cancel(); + } + } + + /** Check that caches matching regex filters in config, are added to CDC after its creation. + * Active/Passive mode means changes made only in one cluster. */ + @Test + public void testActivePassiveReplicationWithRegexFilters() throws Exception { + Set includeTemplates = new HashSet<>(Arrays.asList(REGEX_INCLUDE_PATTERN)); + Set excludeTemplates = new HashSet<>(Arrays.asList(REGEX_EXCLUDE_PATTERN)); + + //Start CDC with only 'active-active-cache' in 'caches' property of CDC config + List> futs = startActivePassiveCdcWithFilters(ACTIVE_PASSIVE_CACHE, + includeTemplates, excludeTemplates); + + try { + createCache(destCluster[0], ACTIVE_PASSIVE_CACHE); + + IgniteCache destCache = createCache(destCluster[0], REGEX_INCLUDE_TEMPLATE_CACHE); + + // Updates for "ignored-cache" should be ignored because of CDC consume configuration. + runAsync(generateData(IGNORED_CACHE, srcCluster[srcCluster.length - 1], IntStream.range(0, KEYS_CNT))); + runAsync(generateData(REGEX_INCLUDE_TEMPLATE_CACHE, srcCluster[srcCluster.length - 1], IntStream.range(0, KEYS_CNT))); + + IgniteCache srcCache = + createCache(srcCluster[srcCluster.length - 1], REGEX_INCLUDE_TEMPLATE_CACHE); + + waitForSameData(srcCache, destCache, KEYS_CNT, WaitDataMode.EXISTS, futs); + + checkMetricsCount(KEYS_CNT); + checkMetrics(); + + IntStream.range(0, KEYS_CNT).forEach(srcCache::remove); + + waitForSameData(srcCache, destCache, KEYS_CNT, WaitDataMode.REMOVED, futs); + + checkMetrics(); + + assertFalse(destCluster[0].cacheNames().contains(IGNORED_CACHE)); + + checkMetricsCount(2 * KEYS_CNT); + + //Shouldn't add to the replication, otherwise CDC will throw an error + runAsync(generateData(REGEX_EXCLUDE_TEMPLATE_CACHE, srcCluster[srcCluster.length - 1], IntStream.range(0, KEYS_CNT))); + + assertFalse(destCluster[0].cacheNames().contains(REGEX_EXCLUDE_TEMPLATE_CACHE)); + } + finally { + for (IgniteInternalFuture fut : futs) + fut.cancel(); + } + } + /** */ public Runnable generateData(String cacheName, IgniteEx ign, IntStream keys) { return () -> { @@ -688,9 +795,18 @@ protected String[] hostAddresses(IgniteEx[] dest) { /** */ protected abstract List> startActivePassiveCdc(String cache); + /** */ + protected abstract List> startActivePassiveCdcWithFilters(String cache, + Set includeTemplates, + Set excludeTemplates); + /** */ protected abstract List> startActiveActiveCdc(); + /** */ + protected abstract List> startActiveActiveCdcWithFilters(Set includeTemplates, + Set excludeTemplates); + /** */ protected abstract void checkConsumerMetrics(Function longMetric); diff --git a/modules/cdc-ext/src/test/java/org/apache/ignite/cdc/CdcIgniteToIgniteReplicationTest.java b/modules/cdc-ext/src/test/java/org/apache/ignite/cdc/CdcIgniteToIgniteReplicationTest.java index b6d42e240..9c021645c 100644 --- a/modules/cdc-ext/src/test/java/org/apache/ignite/cdc/CdcIgniteToIgniteReplicationTest.java +++ b/modules/cdc-ext/src/test/java/org/apache/ignite/cdc/CdcIgniteToIgniteReplicationTest.java @@ -20,6 +20,7 @@ import java.util.ArrayList; import java.util.Collections; import java.util.List; +import java.util.Set; import java.util.function.Function; import org.apache.ignite.Ignition; import org.apache.ignite.cdc.thin.IgniteToIgniteClientCdcStreamer; @@ -45,26 +46,40 @@ public class CdcIgniteToIgniteReplicationTest extends AbstractReplicationTest { /** {@inheritDoc} */ @Override protected List> startActivePassiveCdc(String cache) { + return startActivePassiveCdcWithFilters(cache, Collections.emptySet(), Collections.emptySet()); + } + + /** {@inheritDoc} */ + @Override protected List> startActivePassiveCdcWithFilters(String cache, + Set includeTemplates, + Set excludeTemplates) { List> futs = new ArrayList<>(); for (int i = 0; i < srcCluster.length; i++) - futs.add(igniteToIgnite(srcCluster[i].configuration(), destClusterCliCfg[i], destCluster, cache, "ignite-to-ignite-src-" + i)); + futs.add(igniteToIgnite(srcCluster[i].configuration(), destClusterCliCfg[i], destCluster, cache, + includeTemplates, excludeTemplates, "ignite-to-ignite-src-" + i)); return futs; } /** {@inheritDoc} */ @Override protected List> startActiveActiveCdc() { + return startActiveActiveCdcWithFilters(Collections.emptySet(), Collections.emptySet()); + } + + /** {@inheritDoc} */ + @Override protected List> startActiveActiveCdcWithFilters(Set includeTemplates, + Set excludeTemplates) { List> futs = new ArrayList<>(); for (int i = 0; i < srcCluster.length; i++) { - futs.add(igniteToIgnite( - srcCluster[i].configuration(), destClusterCliCfg[i], destCluster, ACTIVE_ACTIVE_CACHE, "ignite-to-ignite-src-" + i)); + futs.add(igniteToIgnite(srcCluster[i].configuration(), destClusterCliCfg[i], destCluster, + ACTIVE_ACTIVE_CACHE, includeTemplates, excludeTemplates, "ignite-to-ignite-src-" + i)); } for (int i = 0; i < destCluster.length; i++) { - futs.add(igniteToIgnite( - destCluster[i].configuration(), srcClusterCliCfg[i], srcCluster, ACTIVE_ACTIVE_CACHE, "ignite-to-ignite-dest-" + i)); + futs.add(igniteToIgnite(destCluster[i].configuration(), srcClusterCliCfg[i], srcCluster, + ACTIVE_ACTIVE_CACHE, includeTemplates, excludeTemplates, "ignite-to-ignite-dest-" + i)); } return futs; @@ -86,6 +101,8 @@ public class CdcIgniteToIgniteReplicationTest extends AbstractReplicationTest { * @param destCfg Ignite destination cluster configuration. * @param dest Ignite destination cluster. * @param cache Cache name to stream to kafka. + * @param includeTemplates Include regex templates for cache names. + * @param excludeTemplates Exclude regex templates for cache names. * @param threadName Thread to run CDC instance. * @return Future for Change Data Capture application. */ @@ -94,6 +111,8 @@ protected IgniteInternalFuture igniteToIgnite( IgniteConfiguration destCfg, IgniteEx[] dest, String cache, + Set includeTemplates, + Set excludeTemplates, @Nullable String threadName ) { return runAsync(() -> { @@ -115,6 +134,8 @@ protected IgniteInternalFuture igniteToIgnite( streamer.setMaxBatchSize(KEYS_CNT); streamer.setCaches(Collections.singleton(cache)); + streamer.setIncludeTemplates(includeTemplates); + streamer.setExcludeTemplates(excludeTemplates); cdcCfg.setConsumer(streamer); cdcCfg.setMetricExporterSpi(new JmxMetricExporterSpi()); diff --git a/modules/cdc-ext/src/test/java/org/apache/ignite/cdc/RegexFiltersTest.java b/modules/cdc-ext/src/test/java/org/apache/ignite/cdc/RegexFiltersTest.java new file mode 100644 index 000000000..e4007ab08 --- /dev/null +++ b/modules/cdc-ext/src/test/java/org/apache/ignite/cdc/RegexFiltersTest.java @@ -0,0 +1,233 @@ +package org.apache.ignite.cdc; + +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; +import java.util.stream.IntStream; + +import org.apache.ignite.IgniteCache; +import org.apache.ignite.cache.CacheAtomicityMode; +import org.apache.ignite.cache.CacheMode; +import org.apache.ignite.cdc.thin.IgniteToIgniteClientCdcStreamer; +import org.apache.ignite.cluster.ClusterState; +import org.apache.ignite.configuration.CacheConfiguration; +import org.apache.ignite.configuration.ClientConfiguration; +import org.apache.ignite.configuration.DataRegionConfiguration; +import org.apache.ignite.configuration.DataStorageConfiguration; +import org.apache.ignite.configuration.IgniteConfiguration; +import org.apache.ignite.internal.IgniteEx; +import org.apache.ignite.internal.IgniteInternalFuture; +import org.apache.ignite.internal.IgniteInterruptedCheckedException; +import org.apache.ignite.internal.cdc.CdcMain; +import org.apache.ignite.internal.processors.odbc.ClientListenerProcessor; +import org.apache.ignite.internal.util.typedef.F; +import org.apache.ignite.internal.util.typedef.X; +import org.apache.ignite.spi.discovery.tcp.TcpDiscoverySpi; +import org.apache.ignite.spi.discovery.tcp.ipfinder.vm.TcpDiscoveryVmIpFinder; +import org.apache.ignite.spi.metric.jmx.JmxMetricExporterSpi; +import org.apache.ignite.testframework.junits.common.GridCommonAbstractTest; +import org.junit.Test; + +import static org.apache.ignite.spi.communication.tcp.TcpCommunicationSpi.DFLT_PORT_RANGE; +import static org.apache.ignite.testframework.GridTestUtils.runAsync; +import static org.apache.ignite.testframework.GridTestUtils.waitForCondition; + +/** */ +public class RegexFiltersTest extends GridCommonAbstractTest { + + /** */ + private IgniteEx src; + + /** */ + private IgniteEx dest; + + /** */ + private int discoPort = TcpDiscoverySpi.DFLT_PORT; + + /** */ + private enum WaitDataMode { + /** */ + EXISTS, + + /** */ + REMOVED + } + + /** */ + private static final String TEST_CACHE = "test-cache"; + + /** */ + private static final String REGEX_MATCHING_CACHE = "regex-cache"; + + /** */ + private static final String REGEX_INCLUDE_PATTERN = "regex.*"; + + /** */ + private Set includeTemplates; + + /** */ + private static final int KEYS_CNT = 1000; + + /** {@inheritDoc} */ + @Override protected IgniteConfiguration getConfiguration(String igniteInstanceName) throws Exception { + TcpDiscoveryVmIpFinder finder = new TcpDiscoveryVmIpFinder() + .setAddresses(Collections.singleton("127.0.0.1:" + discoPort + ".." + (discoPort + DFLT_PORT_RANGE))); + + IgniteConfiguration cfg = super.getConfiguration(igniteInstanceName) + .setDiscoverySpi(new TcpDiscoverySpi() + .setLocalPort(discoPort) + .setIpFinder(finder)); + + cfg.setDataStorageConfiguration(new DataStorageConfiguration() + .setDefaultDataRegionConfiguration(new DataRegionConfiguration() + .setPersistenceEnabled(true) + .setCdcEnabled(true))); + + cfg.getDataStorageConfiguration() + .setWalForceArchiveTimeout(5_000); + + cfg.setConsistentId(igniteInstanceName); + + return cfg; + } + + /** + * + * @param srcCfg Ignite source node configuration. + * @param cache Cache name to stream to Ignite2Ignite. + * @param includeTemplates Include cache templates. + * @param excludeTemplates Exclude cache templates. + * @return Future for Change Data Capture application. + */ + private IgniteInternalFuture startCdc(IgniteConfiguration srcCfg, + String cache, + Set includeTemplates, + Set excludeTemplates) { + return runAsync(() -> { + CdcConfiguration cdcCfg = new CdcConfiguration(); + + AbstractIgniteCdcStreamer streamer = new IgniteToIgniteClientCdcStreamer() + .setDestinationClientConfiguration(new ClientConfiguration() + .setAddresses(F.first(dest.localNode().addresses()) + ":" + + dest.localNode().attribute(ClientListenerProcessor.CLIENT_LISTENER_PORT))); + + streamer.setMaxBatchSize(KEYS_CNT); + streamer.setCaches(Collections.singleton(cache)); + streamer.setIncludeTemplates(includeTemplates); + streamer.setExcludeTemplates(excludeTemplates); + + cdcCfg.setConsumer(streamer); + cdcCfg.setMetricExporterSpi(new JmxMetricExporterSpi()); + + CdcMain cdc = new CdcMain(srcCfg, null, cdcCfg); + + cdc.run(); + }); + } + + /** {@inheritDoc} */ + @Override protected void beforeTest() throws Exception { + cleanPersistenceDir(); + + src = startGrid(getConfiguration("source-cluster")); + + discoPort += DFLT_PORT_RANGE + 1; + + dest = startGrid(getConfiguration("dest-cluster")); + + includeTemplates = new HashSet<>(Arrays.asList(REGEX_INCLUDE_PATTERN)); + } + + /** {@inheritDoc} */ + @Override protected void afterTest() throws Exception { + stopAllGrids(); + + cleanPersistenceDir(); + } + + /** */ + public void waitForSameData( + IgniteCache src, + IgniteCache dest, + int keysCnt, + WaitDataMode mode, + IgniteInternalFuture fut + ) throws IgniteInterruptedCheckedException { + assertTrue(waitForCondition(() -> { + for (int i = 0; i < keysCnt; i++) { + if (mode == WaitDataMode.EXISTS) { + if (!src.containsKey(i) || !dest.containsKey(i)) + return checkFut(false, fut); + } + else if (mode == WaitDataMode.REMOVED) { + if (src.containsKey(i) || dest.containsKey(i)) + return checkFut(false, fut); + + continue; + } + else + throw new IllegalArgumentException(mode + " not supported."); + + Integer data = dest.get(i); + + if (!data.equals(src.get(i))) + return checkFut(false, fut); + } + + return checkFut(true, fut); + }, getTestTimeout())); + } + + /** */ + private boolean checkFut(boolean res, IgniteInternalFuture fut) { + assertFalse("Fut error: " + X.getFullStackTrace(fut.error()), fut.isDone()); + + return res; + } + + /** */ + public Runnable generateData(IgniteCache cache, IntStream keys) { + return () -> { + keys.forEach(i -> cache.put(i, i * 2)); + }; + } + + /** + * Test checks whether caches added by regex filters are saved to and read from file after CDC restart. + */ + @Test + public void testRegexFiltersOnCdcRestart() throws Exception { + + src.cluster().state(ClusterState.ACTIVE); + + dest.cluster().state(ClusterState.ACTIVE); + + //Start CDC only with 'test-cache' in config and cache masks (regex filters) + IgniteInternalFuture cdc = startCdc(src.configuration(), TEST_CACHE, includeTemplates, Collections.emptySet()); + + IgniteCache srcCache = src.getOrCreateCache(new CacheConfiguration() + .setName(REGEX_MATCHING_CACHE) + .setCacheMode(CacheMode.PARTITIONED) + .setAtomicityMode(CacheAtomicityMode.TRANSACTIONAL)); + + IgniteCache destCache = dest.getOrCreateCache(new CacheConfiguration() + .setName(REGEX_MATCHING_CACHE) + .setCacheMode(CacheMode.PARTITIONED) + .setAtomicityMode(CacheAtomicityMode.TRANSACTIONAL)); + + cdc.cancel(); + + //Restart CDC + IgniteInternalFuture cdc2 = startCdc(src.configuration(), TEST_CACHE, includeTemplates, Collections.emptySet()); + + try { + runAsync(generateData(srcCache, IntStream.range(0, KEYS_CNT))); + + waitForSameData(srcCache, destCache, KEYS_CNT, WaitDataMode.EXISTS, cdc2); + } + finally { + cdc2.cancel(); + } + } +} diff --git a/modules/cdc-ext/src/test/java/org/apache/ignite/cdc/kafka/CdcKafkaReplicationAppsTest.java b/modules/cdc-ext/src/test/java/org/apache/ignite/cdc/kafka/CdcKafkaReplicationAppsTest.java index 927a7b253..6b252584f 100644 --- a/modules/cdc-ext/src/test/java/org/apache/ignite/cdc/kafka/CdcKafkaReplicationAppsTest.java +++ b/modules/cdc-ext/src/test/java/org/apache/ignite/cdc/kafka/CdcKafkaReplicationAppsTest.java @@ -25,6 +25,7 @@ import java.util.Arrays; import java.util.HashMap; import java.util.Map; +import java.util.Set; import java.util.stream.Collectors; import org.apache.ignite.configuration.IgniteConfiguration; import org.apache.ignite.internal.IgniteEx; @@ -113,6 +114,8 @@ public class CdcKafkaReplicationAppsTest extends CdcKafkaReplicationTest { String topic, String metadataTopic, String cache, + Set includeTemplates, + Set excludeTemplates, String threadName ) { Map params = new HashMap<>(); @@ -141,6 +144,8 @@ public class CdcKafkaReplicationAppsTest extends CdcKafkaReplicationTest { IgniteEx[] dest, int partFrom, int partTo, + Set includeTemplates, + Set excludeTemplates, String threadName ) { Map params = new HashMap<>(); diff --git a/modules/cdc-ext/src/test/java/org/apache/ignite/cdc/kafka/CdcKafkaReplicationTest.java b/modules/cdc-ext/src/test/java/org/apache/ignite/cdc/kafka/CdcKafkaReplicationTest.java index a56b2941b..773f1cec0 100644 --- a/modules/cdc-ext/src/test/java/org/apache/ignite/cdc/kafka/CdcKafkaReplicationTest.java +++ b/modules/cdc-ext/src/test/java/org/apache/ignite/cdc/kafka/CdcKafkaReplicationTest.java @@ -21,6 +21,7 @@ import java.util.Collections; import java.util.List; import java.util.Properties; +import java.util.Set; import java.util.function.Function; import java.util.function.Supplier; import javax.management.DynamicMBean; @@ -93,6 +94,13 @@ public class CdcKafkaReplicationTest extends AbstractReplicationTest { /** {@inheritDoc} */ @Override protected List> startActivePassiveCdc(String cache) { + return startActivePassiveCdcWithFilters(cache, Collections.emptySet(), Collections.emptySet()); + } + + /** {@inheritDoc} */ + @Override protected List> startActivePassiveCdcWithFilters(String cache, + Set includeTemplates, + Set excludeTemplates) { try { KAFKA.createTopic(cache, DFLT_PARTS, 1); @@ -107,7 +115,7 @@ public class CdcKafkaReplicationTest extends AbstractReplicationTest { for (IgniteEx ex : srcCluster) { int idx = getTestIgniteInstanceIndex(ex.name()); - futs.add(igniteToKafka(ex.configuration(), cache, SRC_DEST_META_TOPIC, cache, "ignite-src-to-kafka-" + idx)); + futs.add(igniteToKafka(ex.configuration(), cache, SRC_DEST_META_TOPIC, cache, includeTemplates, excludeTemplates, "ignite-src-to-kafka-" + idx)); } for (int i = 0; i < destCluster.length; i++) { @@ -119,7 +127,9 @@ public class CdcKafkaReplicationTest extends AbstractReplicationTest { destCluster, i * (DFLT_PARTS / 2), (i + 1) * (DFLT_PARTS / 2), - "kafka-to-ignite-dest-" + i + includeTemplates, + excludeTemplates, + "kafka-to-ignite-dest-" + i )); } @@ -128,20 +138,26 @@ public class CdcKafkaReplicationTest extends AbstractReplicationTest { /** {@inheritDoc} */ @Override protected List> startActiveActiveCdc() { + return startActiveActiveCdcWithFilters(Collections.emptySet(), Collections.emptySet()); + } + + /** {@inheritDoc} */ + @Override protected List> startActiveActiveCdcWithFilters(Set includeTemplates, + Set excludeTemplates) { List> futs = new ArrayList<>(); for (IgniteEx ex : srcCluster) { int idx = getTestIgniteInstanceIndex(ex.name()); - - futs.add(igniteToKafka( - ex.configuration(), SRC_DEST_TOPIC, SRC_DEST_META_TOPIC, ACTIVE_ACTIVE_CACHE, "ignite-src-to-kafka-" + idx)); + + futs.add(igniteToKafka(ex.configuration(), SRC_DEST_TOPIC, SRC_DEST_META_TOPIC, ACTIVE_ACTIVE_CACHE, includeTemplates, + excludeTemplates, "ignite-src-to-kafka-" + idx)); } for (IgniteEx ex : destCluster) { int idx = getTestIgniteInstanceIndex(ex.name()); - - futs.add(igniteToKafka( - ex.configuration(), DEST_SRC_TOPIC, DEST_SRC_META_TOPIC, ACTIVE_ACTIVE_CACHE, "ignite-dest-to-kafka-" + idx)); + + futs.add(igniteToKafka(ex.configuration(), DEST_SRC_TOPIC, DEST_SRC_META_TOPIC, ACTIVE_ACTIVE_CACHE, includeTemplates, + excludeTemplates, "ignite-dest-to-kafka-" + idx)); } futs.add(kafkaToIgnite( @@ -152,6 +168,8 @@ public class CdcKafkaReplicationTest extends AbstractReplicationTest { destCluster, 0, DFLT_PARTS, + includeTemplates, + excludeTemplates, "kafka-to-ignite-src" )); @@ -163,6 +181,8 @@ public class CdcKafkaReplicationTest extends AbstractReplicationTest { srcCluster, 0, DFLT_PARTS, + includeTemplates, + excludeTemplates, "kafka-to-ignite-dest" )); @@ -255,25 +275,31 @@ private void checkK2IMetrics(Function longMetric) { * @param topic Kafka topic name. * @param metadataTopic Metadata topic name. * @param cache Cache name to stream to kafka. + * @param includeTemplates Include regex templates for cache names. + * @param excludeTemplates Exclude regex templates for cache names. * @return Future for Change Data Capture application. */ protected IgniteInternalFuture igniteToKafka( - IgniteConfiguration igniteCfg, - String topic, - String metadataTopic, - String cache, - String threadName + IgniteConfiguration igniteCfg, + String topic, + String metadataTopic, + String cache, + Set includeTemplates, + Set excludeTemplates, + String threadName ) { return runAsync(() -> { IgniteToKafkaCdcStreamer cdcCnsmr = new IgniteToKafkaCdcStreamer() - .setTopic(topic) - .setMetadataTopic(metadataTopic) - .setKafkaPartitions(DFLT_PARTS) - .setCaches(Collections.singleton(cache)) - .setMaxBatchSize(KEYS_CNT) - .setOnlyPrimary(false) - .setKafkaProperties(kafkaProperties()) - .setKafkaRequestTimeout(DFLT_KAFKA_REQ_TIMEOUT); + .setTopic(topic) + .setMetadataTopic(metadataTopic) + .setKafkaPartitions(DFLT_PARTS) + .setCaches(Collections.singleton(cache)) + .setIncludeTemplates(includeTemplates) + .setExcludeTemplates(excludeTemplates) + .setMaxBatchSize(KEYS_CNT) + .setOnlyPrimary(false) + .setKafkaProperties(kafkaProperties()) + .setKafkaRequestTimeout(DFLT_KAFKA_REQ_TIMEOUT); CdcConfiguration cdcCfg = new CdcConfiguration(); @@ -292,6 +318,8 @@ protected IgniteInternalFuture igniteToKafka( * @param cacheName Cache name. * @param igniteCfg Ignite configuration. * @param dest Destination Ignite cluster. + * @param includeTemplates Include regex templates for cache names. + * @param excludeTemplates Exclude regex templates for cache names. * @return Future for runed {@link KafkaToIgniteCdcStreamer}. */ protected IgniteInternalFuture kafkaToIgnite( @@ -302,6 +330,8 @@ protected IgniteInternalFuture kafkaToIgnite( IgniteEx[] dest, int fromPart, int toPart, + Set includeTemplates, + Set excludeTemplates, String threadName ) { KafkaToIgniteCdcStreamerConfiguration cfg = new KafkaToIgniteCdcStreamerConfiguration(); @@ -311,6 +341,8 @@ protected IgniteInternalFuture kafkaToIgnite( cfg.setThreadCount((toPart - fromPart) / 2); cfg.setCaches(Collections.singletonList(cacheName)); + cfg.setIncludeTemplates(includeTemplates); + cfg.setExcludeTemplates(excludeTemplates); cfg.setTopic(topic); cfg.setMetadataTopic(metadataTopic); cfg.setKafkaRequestTimeout(DFLT_KAFKA_REQ_TIMEOUT); diff --git a/modules/cdc-ext/src/test/java/org/apache/ignite/cdc/kafka/KafkaToIgniteMetadataUpdaterTest.java b/modules/cdc-ext/src/test/java/org/apache/ignite/cdc/kafka/KafkaToIgniteMetadataUpdaterTest.java index 5dc764fea..e6892204e 100644 --- a/modules/cdc-ext/src/test/java/org/apache/ignite/cdc/kafka/KafkaToIgniteMetadataUpdaterTest.java +++ b/modules/cdc-ext/src/test/java/org/apache/ignite/cdc/kafka/KafkaToIgniteMetadataUpdaterTest.java @@ -155,7 +155,7 @@ private IgniteToKafkaCdcStreamer igniteToKafkaCdcStreamer() { GridTestUtils.setFieldValue(streamer, "log", listeningLog.getLogger(IgniteToKafkaCdcStreamer.class)); - streamer.start(new MetricRegistryImpl("test", null, null, log)); + streamer.start(new MetricRegistryImpl("test", null, null, log), null); return streamer; } From 2517c258a85466f82a64b6c5ddae9ea4dec44dd9 Mon Sep 17 00:00:00 2001 From: Andrei Nadyktov Date: Fri, 11 Oct 2024 16:28:09 +0300 Subject: [PATCH 2/4] IGNITE-22530 Make caches set in KafkaToIgniteCdcStreamerApplier mutable --- .../cdc/kafka/AbstractKafkaToIgniteCdcStreamer.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/modules/cdc-ext/src/main/java/org/apache/ignite/cdc/kafka/AbstractKafkaToIgniteCdcStreamer.java b/modules/cdc-ext/src/main/java/org/apache/ignite/cdc/kafka/AbstractKafkaToIgniteCdcStreamer.java index da01403d6..40102a50c 100644 --- a/modules/cdc-ext/src/main/java/org/apache/ignite/cdc/kafka/AbstractKafkaToIgniteCdcStreamer.java +++ b/modules/cdc-ext/src/main/java/org/apache/ignite/cdc/kafka/AbstractKafkaToIgniteCdcStreamer.java @@ -19,11 +19,11 @@ import java.util.ArrayList; import java.util.Collection; +import java.util.HashSet; import java.util.List; import java.util.Properties; import java.util.Set; import java.util.concurrent.atomic.AtomicBoolean; -import java.util.stream.Collectors; import org.apache.ignite.IgniteException; import org.apache.ignite.IgniteLogger; import org.apache.ignite.cdc.AbstractCdcEventsApplier; @@ -152,13 +152,13 @@ public AbstractKafkaToIgniteCdcStreamer(Properties kafkaProps, KafkaToIgniteCdcS protected void runAppliers() { AtomicBoolean stopped = new AtomicBoolean(); - Set caches = null; + Set caches = new HashSet<>(); if (!F.isEmpty(streamerCfg.getCaches())) { checkCaches(streamerCfg.getCaches()); - caches = streamerCfg.getCaches().stream() - .map(CU::cacheId).collect(Collectors.toSet()); + streamerCfg.getCaches().stream() + .map(CU::cacheId).forEach(caches::add); } KafkaToIgniteMetadataUpdater metaUpdr = new KafkaToIgniteMetadataUpdater( From 7d30f0e298dc1840aaee2fbd270c70ff9590462b Mon Sep 17 00:00:00 2001 From: Andrei Nadyktov Date: Sat, 12 Oct 2024 22:18:13 +0300 Subject: [PATCH 3/4] IGNITE-22530 Add removal of destroyed caches from cacheList file --- .../ignite/cdc/AbstractIgniteCdcStreamer.java | 44 +++++++++++++++++-- .../cdc/kafka/IgniteToKafkaCdcStreamer.java | 44 +++++++++++++++++-- 2 files changed, 82 insertions(+), 6 deletions(-) diff --git a/modules/cdc-ext/src/main/java/org/apache/ignite/cdc/AbstractIgniteCdcStreamer.java b/modules/cdc-ext/src/main/java/org/apache/ignite/cdc/AbstractIgniteCdcStreamer.java index a2f830f28..1fd6cba2b 100644 --- a/modules/cdc-ext/src/main/java/org/apache/ignite/cdc/AbstractIgniteCdcStreamer.java +++ b/modules/cdc-ext/src/main/java/org/apache/ignite/cdc/AbstractIgniteCdcStreamer.java @@ -25,6 +25,7 @@ import java.util.HashSet; import java.util.Iterator; import java.util.List; +import java.util.Optional; import java.util.Set; import java.util.regex.Pattern; import java.util.regex.PatternSyntaxException; @@ -283,9 +284,46 @@ private boolean matchesFilters(String cacheName) { /** {@inheritDoc} */ @Override public void onCacheDestroy(Iterator caches) { - caches.forEachRemaining(e -> { - // Just skip. Handle of cache events not supported. - }); + caches.forEachRemaining(this::deleteRegexpCacheIfPresent); + } + + /** + * Removes cache added by regexp from cache list, if this cache is present in file, to prevent file size overflow. + * + * @param cacheId Cache id. + */ + private void deleteRegexpCacheIfPresent(Integer cacheId) { + try { + List caches = loadCaches(); + + Optional cacheName = caches.stream() + .filter(name -> CU.cacheId(name) == cacheId) + .findAny(); + + if (cacheName.isPresent()) { + String name = cacheName.get(); + + caches.remove(name); + + Path savedCachesPath = cdcDir.resolve(SAVED_CACHES_FILE); + + StringBuilder cacheList = new StringBuilder(); + + for (String cache : caches) { + cacheList.append(cache); + + cacheList.append('\n'); + } + + Files.write(savedCachesPath, cacheList.toString().getBytes()); + + if (log.isInfoEnabled()) + log.info("Cache has been removed from replication [cacheName=" + name + ']'); + } + } + catch (IOException e) { + throw new IgniteException(e); + } } /** {@inheritDoc} */ diff --git a/modules/cdc-ext/src/main/java/org/apache/ignite/cdc/kafka/IgniteToKafkaCdcStreamer.java b/modules/cdc-ext/src/main/java/org/apache/ignite/cdc/kafka/IgniteToKafkaCdcStreamer.java index 9d60cbe0f..8baf5a0b2 100644 --- a/modules/cdc-ext/src/main/java/org/apache/ignite/cdc/kafka/IgniteToKafkaCdcStreamer.java +++ b/modules/cdc-ext/src/main/java/org/apache/ignite/cdc/kafka/IgniteToKafkaCdcStreamer.java @@ -27,6 +27,7 @@ import java.util.HashSet; import java.util.Iterator; import java.util.List; +import java.util.Optional; import java.util.Properties; import java.util.Set; import java.util.concurrent.ExecutionException; @@ -282,9 +283,46 @@ public class IgniteToKafkaCdcStreamer implements CdcConsumer { /** {@inheritDoc} */ @Override public void onCacheDestroy(Iterator caches) { - caches.forEachRemaining(e -> { - // Just skip. Handle of cache events not supported. - }); + caches.forEachRemaining(this::deleteRegexpCacheIfPresent); + } + + /** + * Removes cache added by regexp from cache list, if this cache is present in file, to prevent file size overflow. + * + * @param cacheId Cache id. + */ + private void deleteRegexpCacheIfPresent(Integer cacheId) { + try { + List caches = loadCaches(); + + Optional cacheName = caches.stream() + .filter(name -> CU.cacheId(name) == cacheId) + .findAny(); + + if (cacheName.isPresent()) { + String name = cacheName.get(); + + caches.remove(name); + + Path savedCachesPath = cdcDir.resolve(SAVED_CACHES_FILE); + + StringBuilder cacheList = new StringBuilder(); + + for (String cache : caches) { + cacheList.append(cache); + + cacheList.append('\n'); + } + + Files.write(savedCachesPath, cacheList.toString().getBytes()); + + if (log.isInfoEnabled()) + log.info("Cache has been removed from replication [cacheName=" + name + ']'); + } + } + catch (IOException e) { + throw new IgniteException(e); + } } /** Send marker(meta need to be updated) record to each partition of events topic. */ From 02189576ecd402d5322b823ba2a55053d0b1fb9b Mon Sep 17 00:00:00 2001 From: Andrey Nadyktov Date: Thu, 14 Nov 2024 01:25:23 +0300 Subject: [PATCH 4/4] IGNITE-22530 Add atomic write to caches file --- .../ignite/cdc/AbstractIgniteCdcStreamer.java | 71 ++++++++++--------- .../cdc/kafka/IgniteToKafkaCdcStreamer.java | 71 ++++++++++--------- .../cdc/kafka/CdcKafkaReplicationTest.java | 11 +-- 3 files changed, 84 insertions(+), 69 deletions(-) diff --git a/modules/cdc-ext/src/main/java/org/apache/ignite/cdc/AbstractIgniteCdcStreamer.java b/modules/cdc-ext/src/main/java/org/apache/ignite/cdc/AbstractIgniteCdcStreamer.java index 1fd6cba2b..0553dc683 100644 --- a/modules/cdc-ext/src/main/java/org/apache/ignite/cdc/AbstractIgniteCdcStreamer.java +++ b/modules/cdc-ext/src/main/java/org/apache/ignite/cdc/AbstractIgniteCdcStreamer.java @@ -20,8 +20,6 @@ import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; -import java.nio.file.StandardOpenOption; -import java.util.Collections; import java.util.HashSet; import java.util.Iterator; import java.util.List; @@ -46,6 +44,8 @@ import org.apache.ignite.metric.MetricRegistry; import org.apache.ignite.resources.LoggerResource; +import static java.nio.file.StandardCopyOption.ATOMIC_MOVE; +import static java.nio.file.StandardCopyOption.REPLACE_EXISTING; import static org.apache.ignite.cdc.kafka.IgniteToKafkaCdcStreamer.DFLT_IS_ONLY_PRIMARY; /** @@ -81,6 +81,9 @@ public abstract class AbstractIgniteCdcStreamer implements CdcConsumer { /** File with saved names of caches added by cache masks. */ private static final String SAVED_CACHES_FILE = "caches"; + /** Temporary file with saved names of caches added by cache masks. */ + private static final String SAVED_CACHES_TMP_FILE = "caches_tmp"; + /** CDC directory path. */ private Path cdcDir; @@ -193,7 +196,7 @@ public abstract class AbstractIgniteCdcStreamer implements CdcConsumer { /** * Finds match between cache name and user's regex templates. - * If match found, adds this cache's id to id's list and saves cache name to file. + * If match is found, adds this cache's id to id's list and saves cache name to file. * * @param cacheName Cache name. */ @@ -204,7 +207,11 @@ private void matchWithRegexTemplates(String cacheName) { cachesIds.add(cacheId); try { - saveCache(cacheName); + List caches = loadCaches(); + + caches.add(cacheName); + + save(caches); } catch (IOException e) { throw new IgniteException(e); @@ -216,18 +223,28 @@ private void matchWithRegexTemplates(String cacheName) { } /** - * Writes cache name to file + * Writes caches list to file * - * @param cacheName Cache name. + * @param caches Caches list. */ - private void saveCache(String cacheName) throws IOException { - if (cdcDir != null) { - Path savedCachesPath = cdcDir.resolve(SAVED_CACHES_FILE); + private void save(List caches) throws IOException { + if (cdcDir == null) { + throw new IgniteException("Can't write to '" + SAVED_CACHES_FILE + "' file. Cdc directory is null"); + } + Path savedCachesPath = cdcDir.resolve(SAVED_CACHES_FILE); + Path tmpSavedCachesPath = cdcDir.resolve(SAVED_CACHES_TMP_FILE); - String cn = cacheName + '\n'; + StringBuilder cacheList = new StringBuilder(); - Files.write(savedCachesPath, cn.getBytes(), StandardOpenOption.APPEND); + for (String cache : caches) { + cacheList.append(cache); + + cacheList.append('\n'); } + + Files.write(tmpSavedCachesPath, cacheList.toString().getBytes()); + + Files.move(tmpSavedCachesPath, savedCachesPath, ATOMIC_MOVE, REPLACE_EXISTING); } /** @@ -236,19 +253,19 @@ private void saveCache(String cacheName) throws IOException { * @return List of saved caches names. */ private List loadCaches() throws IOException { - if (cdcDir != null) { - Path savedCachesPath = cdcDir.resolve(SAVED_CACHES_FILE); - - if (Files.notExists(savedCachesPath)) { - Files.createFile(savedCachesPath); + if (cdcDir == null) { + throw new IgniteException("Can't load '" + SAVED_CACHES_FILE + "' file. Cdc directory is null"); + } + Path savedCachesPath = cdcDir.resolve(SAVED_CACHES_FILE); - if (log.isInfoEnabled()) - log.info("Cache list created: " + savedCachesPath); - } + if (Files.notExists(savedCachesPath)) { + Files.createFile(savedCachesPath); - return Files.readAllLines(savedCachesPath); + if (log.isInfoEnabled()) + log.info("Cache list created: " + savedCachesPath); } - return Collections.emptyList(); + + return Files.readAllLines(savedCachesPath); } /** @@ -305,17 +322,7 @@ private void deleteRegexpCacheIfPresent(Integer cacheId) { caches.remove(name); - Path savedCachesPath = cdcDir.resolve(SAVED_CACHES_FILE); - - StringBuilder cacheList = new StringBuilder(); - - for (String cache : caches) { - cacheList.append(cache); - - cacheList.append('\n'); - } - - Files.write(savedCachesPath, cacheList.toString().getBytes()); + save(caches); if (log.isInfoEnabled()) log.info("Cache has been removed from replication [cacheName=" + name + ']'); diff --git a/modules/cdc-ext/src/main/java/org/apache/ignite/cdc/kafka/IgniteToKafkaCdcStreamer.java b/modules/cdc-ext/src/main/java/org/apache/ignite/cdc/kafka/IgniteToKafkaCdcStreamer.java index 8baf5a0b2..1ba3113a0 100644 --- a/modules/cdc-ext/src/main/java/org/apache/ignite/cdc/kafka/IgniteToKafkaCdcStreamer.java +++ b/modules/cdc-ext/src/main/java/org/apache/ignite/cdc/kafka/IgniteToKafkaCdcStreamer.java @@ -20,10 +20,8 @@ import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; -import java.nio.file.StandardOpenOption; import java.util.ArrayList; import java.util.Collection; -import java.util.Collections; import java.util.HashSet; import java.util.Iterator; import java.util.List; @@ -66,6 +64,8 @@ import org.apache.kafka.common.serialization.ByteArraySerializer; import org.apache.kafka.common.serialization.IntegerSerializer; +import static java.nio.file.StandardCopyOption.ATOMIC_MOVE; +import static java.nio.file.StandardCopyOption.REPLACE_EXISTING; import static org.apache.ignite.cdc.kafka.KafkaToIgniteCdcStreamerConfiguration.DFLT_KAFKA_REQ_TIMEOUT; import static org.apache.ignite.cdc.kafka.KafkaToIgniteCdcStreamerConfiguration.DFLT_MAX_BATCH_SIZE; import static org.apache.kafka.clients.producer.ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG; @@ -163,6 +163,9 @@ public class IgniteToKafkaCdcStreamer implements CdcConsumer { /** File with saved names of caches added by cache masks. */ private static final String SAVED_CACHES_FILE = "caches"; + /** Temporary file with saved names of caches added by cache masks. */ + private static final String SAVED_CACHES_TMP_FILE = "caches_tmp"; + /** CDC directory path. */ private Path cdcDir; @@ -304,17 +307,7 @@ private void deleteRegexpCacheIfPresent(Integer cacheId) { caches.remove(name); - Path savedCachesPath = cdcDir.resolve(SAVED_CACHES_FILE); - - StringBuilder cacheList = new StringBuilder(); - - for (String cache : caches) { - cacheList.append(cache); - - cacheList.append('\n'); - } - - Files.write(savedCachesPath, cacheList.toString().getBytes()); + save(caches); if (log.isInfoEnabled()) log.info("Cache has been removed from replication [cacheName=" + name + ']'); @@ -481,19 +474,19 @@ private void prepareRegexFilters() { * @return List of saved caches names. */ private List loadCaches() throws IOException { - if (cdcDir != null) { - Path savedCachesPath = cdcDir.resolve(SAVED_CACHES_FILE); - - if (Files.notExists(savedCachesPath)) { - Files.createFile(savedCachesPath); + if (cdcDir == null) { + throw new IgniteException("Can't load '" + SAVED_CACHES_FILE + "' file. Cdc directory is null"); + } + Path savedCachesPath = cdcDir.resolve(SAVED_CACHES_FILE); - if (log.isInfoEnabled()) - log.info("Cache list created: " + savedCachesPath); - } + if (Files.notExists(savedCachesPath)) { + Files.createFile(savedCachesPath); - return Files.readAllLines(savedCachesPath); + if (log.isInfoEnabled()) + log.info("Cache list created: " + savedCachesPath); } - return Collections.emptyList(); + + return Files.readAllLines(savedCachesPath); } /** @@ -514,7 +507,7 @@ private boolean matchesFilters(String cacheName) { /** * Finds match between cache name and user's regex templates. - * If match found, adds this cache's id to id's list and saves cache name to file. + * If match is found, adds this cache's id to id's list and saves cache name to file. * * @param cacheName Cache name. */ @@ -525,7 +518,11 @@ private void matchWithRegexTemplates(String cacheName) { cachesIds.add(cacheId); try { - saveCache(cacheName); + List caches = loadCaches(); + + caches.add(cacheName); + + save(caches); } catch (IOException e) { throw new IgniteException(e); @@ -537,18 +534,28 @@ private void matchWithRegexTemplates(String cacheName) { } /** - * Writes cache name to file. + * Writes caches list to file * - * @param cacheName Cache name. + * @param caches Caches list. */ - private void saveCache(String cacheName) throws IOException { - if (cdcDir != null) { - Path savedCaches = cdcDir.resolve(SAVED_CACHES_FILE); + private void save(List caches) throws IOException { + if (cdcDir == null) { + throw new IgniteException("Can't write to '" + SAVED_CACHES_FILE + "' file. Cdc directory is null"); + } + Path savedCachesPath = cdcDir.resolve(SAVED_CACHES_FILE); + Path tmpSavedCachesPath = cdcDir.resolve(SAVED_CACHES_TMP_FILE); + + StringBuilder cacheList = new StringBuilder(); - String cn = cacheName + '\n'; + for (String cache : caches) { + cacheList.append(cache); - Files.write(savedCaches, cn.getBytes(), StandardOpenOption.APPEND); + cacheList.append('\n'); } + + Files.write(tmpSavedCachesPath, cacheList.toString().getBytes()); + + Files.move(tmpSavedCachesPath, savedCachesPath, ATOMIC_MOVE, REPLACE_EXISTING); } /** diff --git a/modules/cdc-ext/src/test/java/org/apache/ignite/cdc/kafka/CdcKafkaReplicationTest.java b/modules/cdc-ext/src/test/java/org/apache/ignite/cdc/kafka/CdcKafkaReplicationTest.java index 773f1cec0..72e75ce81 100644 --- a/modules/cdc-ext/src/test/java/org/apache/ignite/cdc/kafka/CdcKafkaReplicationTest.java +++ b/modules/cdc-ext/src/test/java/org/apache/ignite/cdc/kafka/CdcKafkaReplicationTest.java @@ -115,7 +115,8 @@ public class CdcKafkaReplicationTest extends AbstractReplicationTest { for (IgniteEx ex : srcCluster) { int idx = getTestIgniteInstanceIndex(ex.name()); - futs.add(igniteToKafka(ex.configuration(), cache, SRC_DEST_META_TOPIC, cache, includeTemplates, excludeTemplates, "ignite-src-to-kafka-" + idx)); + futs.add(igniteToKafka(ex.configuration(), cache, SRC_DEST_META_TOPIC, cache, includeTemplates, + excludeTemplates, "ignite-src-to-kafka-" + idx)); } for (int i = 0; i < destCluster.length; i++) { @@ -149,15 +150,15 @@ public class CdcKafkaReplicationTest extends AbstractReplicationTest { for (IgniteEx ex : srcCluster) { int idx = getTestIgniteInstanceIndex(ex.name()); - futs.add(igniteToKafka(ex.configuration(), SRC_DEST_TOPIC, SRC_DEST_META_TOPIC, ACTIVE_ACTIVE_CACHE, includeTemplates, - excludeTemplates, "ignite-src-to-kafka-" + idx)); + futs.add(igniteToKafka(ex.configuration(), SRC_DEST_TOPIC, SRC_DEST_META_TOPIC, ACTIVE_ACTIVE_CACHE, + includeTemplates, excludeTemplates, "ignite-src-to-kafka-" + idx)); } for (IgniteEx ex : destCluster) { int idx = getTestIgniteInstanceIndex(ex.name()); - futs.add(igniteToKafka(ex.configuration(), DEST_SRC_TOPIC, DEST_SRC_META_TOPIC, ACTIVE_ACTIVE_CACHE, includeTemplates, - excludeTemplates, "ignite-dest-to-kafka-" + idx)); + futs.add(igniteToKafka(ex.configuration(), DEST_SRC_TOPIC, DEST_SRC_META_TOPIC, ACTIVE_ACTIVE_CACHE, + includeTemplates, excludeTemplates, "ignite-dest-to-kafka-" + idx)); } futs.add(kafkaToIgnite(