This is an automated email from the ASF dual-hosted git repository. rgoers pushed a commit to branch handle-duplicates in repository https://gitbox.apache.org/repos/asf/logging-flume-mongodb.git
commit 5e12f3ab3adcdde4e077d347c4c1f2780f659d05 Author: Ralph Goers <[email protected]> AuthorDate: Tue Sep 1 21:14:10 2026 -0700 handle duplicate events --- .flattened-pom.xml | 216 +++++++++++++++++++++ flume-mongodb-sink/.flattened-pom.xml | 6 + flume-mongodb-sink/pom.xml | 7 + .../flume/sink/mongodb/DefaultMongoDbWriter.java | 38 +++- .../org/apache/flume/sink/mongodb/MongoDbSink.java | 23 ++- .../flume/sink/mongodb/MongoDbSinkCounter.java | 50 +++++ ...{MongoDbWriter.java => MongoDbWriteResult.java} | 38 ++-- .../apache/flume/sink/mongodb/MongoDbWriter.java | 10 +- .../apache/flume/sink/mongodb/TestMongoDbSink.java | 26 ++- .../sink/mongodb/TestMongoDbSinkEmbedded.java | 164 ++++++++++++++++ 10 files changed, 547 insertions(+), 31 deletions(-) diff --git a/.flattened-pom.xml b/.flattened-pom.xml new file mode 100644 index 0000000..815ad8e --- /dev/null +++ b/.flattened-pom.xml @@ -0,0 +1,216 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + ~ Licensed to the Apache Software Foundation (ASF) under one or more + ~ contributor license agreements. See the NOTICE file distributed with + ~ this work for additional information regarding copyright ownership. + ~ The ASF licenses this file to you under the Apache License, Version 2.0 + ~ (the "License"); you may not use this file except in compliance with + ~ the License. You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License. + --> +<project xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd" xmlns="http://maven.apache.org/POM/4.0.0" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"> + <modelVersion>4.0.0</modelVersion> + <parent> + <groupId>org.apache.flume</groupId> + <artifactId>flume-parent</artifactId> + <version>2.0.0-SNAPSHOT</version> + <relativePath></relativePath> + </parent> + <groupId>org.apache.flume</groupId> + <artifactId>flume-mongodb-parent</artifactId> + <version>2.0.0-SNAPSHOT</version> + <packaging>pom</packaging> + <name>Flume MongoDB Parent</name> + <url>https://logging.apache.org/flume/2.x/index.html/flume-mongodb-parent</url> + <inceptionYear>2022</inceptionYear> + <organization> + <name>Apache Software Foundation</name> + <url>http://www.apache.org</url> + </organization> + <licenses> + <license> + <name>The Apache Software License, Version 2.0</name> + <url>http://www.apache.org/licenses/LICENSE-2.0.txt</url> + </license> + </licenses> + <developers> + <developer> + <id>rgoers</id> + <name>Ralph Goers</name> + <email>[email protected]</email> + <organization>Intuit</organization> + </developer> + </developers> + <mailingLists> + <mailingList> + <name>Flume User List</name> + <subscribe>[email protected]</subscribe> + <unsubscribe>[email protected]</unsubscribe> + <post>[email protected]</post> + <archive>http://mail-archives.apache.org/mod_mbox/flume-user/</archive> + </mailingList> + <mailingList> + <name>Flume Developer List</name> + <subscribe>[email protected]</subscribe> + <unsubscribe>[email protected]</unsubscribe> + <post>[email protected]</post> + <archive>http://mail-archives.apache.org/mod_mbox/flume-dev/</archive> + </mailingList> + <mailingList> + <name>Flume Commits</name> + <subscribe>[email protected]</subscribe> + <unsubscribe>[email protected]</unsubscribe> + <post>[email protected]</post> + <archive>http://mail-archives.apache.org/mod_mbox/flume-commits/</archive> + </mailingList> + </mailingLists> + <modules> + <module>flume-mongodb-sink</module> + </modules> + <scm> + <connection>https://gitbox.apache.org/repos/asf/flume-spring-boot.git</connection> + <developerConnection>https://gitbox.apache.org/repos/asf/flume-spring-boot.git</developerConnection> + <url>https://gitbox.apache.org/repos/asf/flume-spring-boot.git</url> + </scm> + <issueManagement> + <system>JIRA</system> + <url>https://issues.apache.org/jira/browse/FLUME</url> + </issueManagement> + <properties> + <mongodb.version>5.10.0</mongodb.version> + <jackson.version>2.17.0</jackson.version> + <findsecbugs-plugin.version>1.12.0</findsecbugs-plugin.version> + <ReleaseVersion>2.0.0</ReleaseVersion> + <ReleaseManager>Ralph Goers</ReleaseManager> + <rat.version>0.12</rat.version> + <log4j.version>2.26.1</log4j.version> + <junit.version>4.13.2</junit.version> + <ReleaseKey>B3D8E1BA</ReleaseKey> + <maven.compiler.source>11</maven.compiler.source> + <spotless-maven-plugin.version>2.27.2</spotless-maven-plugin.version> + <mvn-gpg-plugin.version>1.6</mvn-gpg-plugin.version> + <checksum-maven-plugin.version>1.11</checksum-maven-plugin.version> + <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> + <module.name>org.apache.flume.mongodb</module.name> + <mvn-javadoc-plugin.version>2.9</mvn-javadoc-plugin.version> + <mockito.version>1.9.0</mockito.version> + <dropwizard-metrics.version>4.1.18</dropwizard-metrics.version> + <flume.version>2.0.0-SNAPSHOT</flume.version> + <spotbugs-maven-plugin.version>4.7.2.1</spotbugs-maven-plugin.version> + <maven.compiler.target>11</maven.compiler.target> + <SigningUserName>[email protected]</SigningUserName> + </properties> + <dependencyManagement> + <dependencies> + <dependency> + <groupId>org.apache.flume</groupId> + <artifactId>flume-ng-core</artifactId> + <version>${flume.version}</version> + </dependency> + <dependency> + <groupId>org.apache.flume</groupId> + <artifactId>flume-ng-sdk</artifactId> + <version>${flume.version}</version> + </dependency> + <dependency> + <groupId>org.apache.flume</groupId> + <artifactId>flume-ng-configuration</artifactId> + <version>${flume.version}</version> + </dependency> + <dependency> + <groupId>org.mongodb</groupId> + <artifactId>mongodb-driver-bom</artifactId> + <version>${mongodb.version}</version> + <type>pom</type> + <scope>import</scope> + </dependency> + <dependency> + <groupId>io.dropwizard.metrics</groupId> + <artifactId>metrics-core</artifactId> + <version>${dropwizard-metrics.version}</version> + </dependency> + <dependency> + <groupId>org.apache.logging.log4j</groupId> + <artifactId>log4j-api</artifactId> + <version>${log4j.version}</version> + </dependency> + <dependency> + <groupId>org.apache.logging.log4j</groupId> + <artifactId>log4j-core</artifactId> + <version>${log4j.version}</version> + </dependency> + <dependency> + <groupId>com.fasterxml.jackson.core</groupId> + <artifactId>jackson-core</artifactId> + <version>${jackson.version}</version> + </dependency> + <dependency> + <groupId>junit</groupId> + <artifactId>junit</artifactId> + <version>${junit.version}</version> + </dependency> + <dependency> + <groupId>org.mockito</groupId> + <artifactId>mockito-all</artifactId> + <version>${mockito.version}</version> + <scope>test</scope> + </dependency> + </dependencies> + </dependencyManagement> + <build> + <plugins> + <plugin> + <groupId>org.apache.rat</groupId> + <artifactId>apache-rat-plugin</artifactId> + <version>${rat.version}</version> + <executions> + <execution> + <id>verify.rat</id> + <phase>verify</phase> + <goals> + <goal>check</goal> + </goals> + </execution> + </executions> + <configuration> + <excludes> + <exclude>**/.idea/</exclude> + <exclude>**/*.iml</exclude> + <exclude>src/main/resources/META-INF/services/**/*</exclude> + <exclude>**/nb-configuration.xml</exclude> + <exclude>.git/</exclude> + <exclude>patchprocess/</exclude> + <exclude>.gitignore</exclude> + <exclude>**/*.yml</exclude> + <exclude>**/*.yaml</exclude> + <exclude>**/*.json</exclude> + <exclude>.repository/</exclude> + <exclude>**/*.diff</exclude> + <exclude>**/*.patch</exclude> + <exclude>**/*.avsc</exclude> + <exclude>**/*.avro</exclude> + <exclude>**/docs/**</exclude> + <exclude>**/test/resources/**</exclude> + <exclude>**/.settings/*</exclude> + <exclude>**/.classpath</exclude> + <exclude>**/.project</exclude> + <exclude>**/target/**</exclude> + <exclude>**/derby.log</exclude> + <exclude>**/metastore_db/</exclude> + <exclude>.mvn/**</exclude> + <exclude>**/exclude-pmd.properties</exclude> + </excludes> + <consoleOutput>true</consoleOutput> + </configuration> + </plugin> + </plugins> + </build> +</project> diff --git a/flume-mongodb-sink/.flattened-pom.xml b/flume-mongodb-sink/.flattened-pom.xml index 26355e6..48e0e54 100644 --- a/flume-mongodb-sink/.flattened-pom.xml +++ b/flume-mongodb-sink/.flattened-pom.xml @@ -79,6 +79,12 @@ <artifactId>mockito-all</artifactId> <scope>test</scope> </dependency> + <dependency> + <groupId>de.flapdoodle.embed</groupId> + <artifactId>de.flapdoodle.embed.mongo</artifactId> + <version>3.5.4</version> + <scope>test</scope> + </dependency> </dependencies> <build> <plugins> diff --git a/flume-mongodb-sink/pom.xml b/flume-mongodb-sink/pom.xml index da3c4b8..2f73796 100644 --- a/flume-mongodb-sink/pom.xml +++ b/flume-mongodb-sink/pom.xml @@ -83,6 +83,13 @@ <scope>test</scope> </dependency> + <dependency> + <groupId>de.flapdoodle.embed</groupId> + <artifactId>de.flapdoodle.embed.mongo</artifactId> + <version>3.5.4</version> + <scope>test</scope> + </dependency> + </dependencies> <build> diff --git a/flume-mongodb-sink/src/main/java/org/apache/flume/sink/mongodb/DefaultMongoDbWriter.java b/flume-mongodb-sink/src/main/java/org/apache/flume/sink/mongodb/DefaultMongoDbWriter.java index d039717..28fb4ab 100644 --- a/flume-mongodb-sink/src/main/java/org/apache/flume/sink/mongodb/DefaultMongoDbWriter.java +++ b/flume-mongodb-sink/src/main/java/org/apache/flume/sink/mongodb/DefaultMongoDbWriter.java @@ -16,10 +16,15 @@ */ package org.apache.flume.sink.mongodb; +import com.mongodb.DuplicateKeyException; +import com.mongodb.ErrorCategory; +import com.mongodb.MongoWriteException; import com.mongodb.WriteConcern; import com.mongodb.client.MongoCollection; import com.mongodb.client.MongoDatabase; import java.util.List; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; import org.bson.Document; /** @@ -28,6 +33,8 @@ import org.bson.Document; */ public class DefaultMongoDbWriter implements MongoDbWriter { + private static final Logger logger = LogManager.getLogger(DefaultMongoDbWriter.class); + private final MongoDatabase mongoDatabase; private final WriteConcern writeConcern; @@ -37,12 +44,39 @@ public class DefaultMongoDbWriter implements MongoDbWriter { } @Override - public void write(String collectionName, List<Document> documents) { + public MongoDbWriteResult write(String collectionName, List<Document> documents) { MongoCollection<Document> collection = mongoDatabase.getCollection(collectionName); if (writeConcern != null) { collection = collection.withWriteConcern(writeConcern); } - collection.insertMany(documents); + + long insertedCount = 0; + long duplicateCount = 0; + // Insert one document at a time (rather than insertMany) so that a + // single duplicate key does not abort the rest of the batch. + for (Document document : documents) { + try { + collection.insertOne(document); + insertedCount++; + } catch (DuplicateKeyException ex) { + logger.warn( + "Duplicate key while inserting into collection {}, skipping event: {}", + collectionName, + ex.getMessage()); + duplicateCount++; + } catch (MongoWriteException ex) { + if (ex.getError().getCategory() == ErrorCategory.DUPLICATE_KEY) { + logger.warn( + "Duplicate key while inserting into collection {}, skipping event: {}", + collectionName, + ex.getMessage()); + duplicateCount++; + } else { + throw ex; + } + } + } + return new MongoDbWriteResult(insertedCount, duplicateCount); } @Override diff --git a/flume-mongodb-sink/src/main/java/org/apache/flume/sink/mongodb/MongoDbSink.java b/flume-mongodb-sink/src/main/java/org/apache/flume/sink/mongodb/MongoDbSink.java index 9c32289..adc545a 100644 --- a/flume-mongodb-sink/src/main/java/org/apache/flume/sink/mongodb/MongoDbSink.java +++ b/flume-mongodb-sink/src/main/java/org/apache/flume/sink/mongodb/MongoDbSink.java @@ -34,7 +34,6 @@ import org.apache.flume.Transaction; import org.apache.flume.conf.BatchSizeSupported; import org.apache.flume.conf.Configurable; import org.apache.flume.conf.ConfigurationException; -import org.apache.flume.instrumentation.SinkCounter; import org.apache.flume.sink.AbstractSink; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -83,7 +82,7 @@ public class MongoDbSink extends AbstractSink implements Configurable, BatchSize private MongoClient mongoClient; private MongoDbWriter writer; - private SinkCounter counter; + private MongoDbSinkCounter counter; // For testing public String getDatabaseName() { @@ -94,6 +93,11 @@ public class MongoDbSink extends AbstractSink implements Configurable, BatchSize return defaultCollection; } + /** For testing: number of events skipped due to duplicate keys. */ + public long getDuplicateEventCount() { + return counter.getDuplicateEventCount(); + } + @Override public long getBatchSize() { return batchSize; @@ -138,12 +142,19 @@ public class MongoDbSink extends AbstractSink implements Configurable, BatchSize .add(document); } + long insertedEvents = 0; + long duplicateEvents = 0; for (Map.Entry<String, List<Document>> entry : documentsByCollection.entrySet()) { - writer.write(entry.getKey(), entry.getValue()); + MongoDbWriteResult writeResult = writer.write(entry.getKey(), entry.getValue()); + insertedEvents += writeResult.getInsertedCount(); + duplicateEvents += writeResult.getDuplicateCount(); } - if (processedEvents > 0) { - counter.addToEventDrainSuccessCount(processedEvents); + if (insertedEvents > 0) { + counter.addToEventDrainSuccessCount(insertedEvents); + } + if (duplicateEvents > 0) { + counter.addToDuplicateEventCount(duplicateEvents); } transaction.commit(); @@ -279,7 +290,7 @@ public class MongoDbSink extends AbstractSink implements Configurable, BatchSize } if (counter == null) { - counter = new SinkCounter(getName()); + counter = new MongoDbSinkCounter(getName()); } } } diff --git a/flume-mongodb-sink/src/main/java/org/apache/flume/sink/mongodb/MongoDbSinkCounter.java b/flume-mongodb-sink/src/main/java/org/apache/flume/sink/mongodb/MongoDbSinkCounter.java new file mode 100644 index 0000000..97737d2 --- /dev/null +++ b/flume-mongodb-sink/src/main/java/org/apache/flume/sink/mongodb/MongoDbSinkCounter.java @@ -0,0 +1,50 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to you under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.flume.sink.mongodb; + +import org.apache.flume.instrumentation.SinkCounter; + +/** + * {@link SinkCounter} extension that additionally tracks the number of + * events skipped because they duplicated a document already present in + * MongoDB (i.e. resulted in a {@code com.mongodb.DuplicateKeyException}). + * These events are not counted towards {@code eventDrainSuccessCount} since + * they were not actually inserted, but they should also not be treated as a + * batch failure. + */ +public class MongoDbSinkCounter extends SinkCounter { + + private static final String COUNTER_DUPLICATE_EVENT = "sink.event.duplicate"; + + private static final String[] ATTRIBUTES = {COUNTER_DUPLICATE_EVENT}; + + public MongoDbSinkCounter(String name) { + super(name, ATTRIBUTES); + } + + public long getDuplicateEventCount() { + return get(COUNTER_DUPLICATE_EVENT); + } + + public long incrementDuplicateEventCount() { + return increment(COUNTER_DUPLICATE_EVENT); + } + + public long addToDuplicateEventCount(long delta) { + return addAndGet(COUNTER_DUPLICATE_EVENT, delta); + } +} diff --git a/flume-mongodb-sink/src/main/java/org/apache/flume/sink/mongodb/MongoDbWriter.java b/flume-mongodb-sink/src/main/java/org/apache/flume/sink/mongodb/MongoDbWriteResult.java similarity index 53% copy from flume-mongodb-sink/src/main/java/org/apache/flume/sink/mongodb/MongoDbWriter.java copy to flume-mongodb-sink/src/main/java/org/apache/flume/sink/mongodb/MongoDbWriteResult.java index 005fb53..d697119 100644 --- a/flume-mongodb-sink/src/main/java/org/apache/flume/sink/mongodb/MongoDbWriter.java +++ b/flume-mongodb-sink/src/main/java/org/apache/flume/sink/mongodb/MongoDbWriteResult.java @@ -16,28 +16,30 @@ */ package org.apache.flume.sink.mongodb; -import java.util.List; -import org.bson.Document; - /** - * Abstraction over the MongoDB write path used by {@link MongoDbSink}. - * Separating this out of the sink keeps the routing/document-construction - * logic in {@link MongoDbSink} independent of, and easily testable without, - * a live MongoDB connection. + * Outcome of writing a batch of documents to a single MongoDB collection. */ -public interface MongoDbWriter { +public final class MongoDbWriteResult { - /** - * Writes the given documents to the named collection. - * - * @param collectionName the target collection name - * @param documents the documents to insert, in order - */ - void write(String collectionName, List<Document> documents); + private final long insertedCount; + private final long duplicateCount; + + public MongoDbWriteResult(long insertedCount, long duplicateCount) { + this.insertedCount = insertedCount; + this.duplicateCount = duplicateCount; + } + + /** Number of documents that were successfully inserted. */ + public long getInsertedCount() { + return insertedCount; + } /** - * Releases any resources (e.g. the underlying MongoDB client) held by - * this writer. + * Number of documents that were skipped because they duplicated a + * document that already existed in the collection (i.e. triggered a + * {@code com.mongodb.DuplicateKeyException}). */ - void close(); + public long getDuplicateCount() { + return duplicateCount; + } } diff --git a/flume-mongodb-sink/src/main/java/org/apache/flume/sink/mongodb/MongoDbWriter.java b/flume-mongodb-sink/src/main/java/org/apache/flume/sink/mongodb/MongoDbWriter.java index 005fb53..d458bc6 100644 --- a/flume-mongodb-sink/src/main/java/org/apache/flume/sink/mongodb/MongoDbWriter.java +++ b/flume-mongodb-sink/src/main/java/org/apache/flume/sink/mongodb/MongoDbWriter.java @@ -28,12 +28,18 @@ import org.bson.Document; public interface MongoDbWriter { /** - * Writes the given documents to the named collection. + * Writes the given documents to the named collection. Documents that + * fail to insert because they duplicate an existing document (i.e. + * trigger a {@code com.mongodb.DuplicateKeyException}) are skipped + * rather than causing the whole batch to fail; they are reported via + * {@link MongoDbWriteResult#getDuplicateCount()}. * * @param collectionName the target collection name * @param documents the documents to insert, in order + * @return the number of documents inserted and the number skipped as + * duplicates */ - void write(String collectionName, List<Document> documents); + MongoDbWriteResult write(String collectionName, List<Document> documents); /** * Releases any resources (e.g. the underlying MongoDB client) held by diff --git a/flume-mongodb-sink/src/test/java/org/apache/flume/sink/mongodb/TestMongoDbSink.java b/flume-mongodb-sink/src/test/java/org/apache/flume/sink/mongodb/TestMongoDbSink.java index a2d2846..38f3bd7 100644 --- a/flume-mongodb-sink/src/test/java/org/apache/flume/sink/mongodb/TestMongoDbSink.java +++ b/flume-mongodb-sink/src/test/java/org/apache/flume/sink/mongodb/TestMongoDbSink.java @@ -36,7 +36,6 @@ import org.apache.flume.channel.MemoryChannel; import org.apache.flume.conf.Configurables; import org.apache.flume.conf.ConfigurationException; import org.apache.flume.event.EventBuilder; -import org.apache.flume.instrumentation.SinkCounter; import org.bson.Document; import org.junit.Test; import org.mockito.internal.util.reflection.Whitebox; @@ -50,10 +49,14 @@ public class TestMongoDbSink { private static final class FakeMongoDbWriter implements MongoDbWriter { private final Map<String, List<Document>> written = new LinkedHashMap<>(); private boolean closed = false; + private long duplicateCountToReport = 0; @Override - public void write(String collectionName, List<Document> documents) { + public MongoDbWriteResult write(String collectionName, List<Document> documents) { written.computeIfAbsent(collectionName, k -> new ArrayList<>()).addAll(documents); + long duplicates = Math.min(duplicateCountToReport, documents.size()); + duplicateCountToReport -= duplicates; + return new MongoDbWriteResult(documents.size() - duplicates, duplicates); } @Override @@ -78,7 +81,7 @@ public class TestMongoDbSink { channel.start(); Configurables.configure(sink, context); Whitebox.setInternalState(sink, "writer", writer); - Whitebox.setInternalState(sink, "counter", new SinkCounter("test")); + Whitebox.setInternalState(sink, "counter", new MongoDbSinkCounter("test")); return sink; } @@ -114,6 +117,23 @@ public class TestMongoDbSink { new MongoDbSink().configure(context); } + @Test + public void testDuplicateEventsDoNotFailBatchAndAreCountedSeparately() throws EventDeliveryException { + FakeMongoDbWriter writer = new FakeMongoDbWriter(); + writer.duplicateCountToReport = 1; + Context context = baseContext(); + MongoDbSink sink = createSink(context, writer); + Channel channel = sink.getChannel(); + + putEvent(channel, "{\"foo\":\"1\"}".getBytes(StandardCharsets.UTF_8), new HashMap<String, String>()); + putEvent(channel, "{\"foo\":\"2\"}".getBytes(StandardCharsets.UTF_8), new HashMap<String, String>()); + + Sink.Status status = sink.process(); + + assertEquals(Sink.Status.READY, status); + assertEquals(1, sink.getDuplicateEventCount()); + } + @Test public void testWritesToDefaultCollectionWhenNoHeaderConfigured() throws EventDeliveryException { FakeMongoDbWriter writer = new FakeMongoDbWriter(); diff --git a/flume-mongodb-sink/src/test/java/org/apache/flume/sink/mongodb/TestMongoDbSinkEmbedded.java b/flume-mongodb-sink/src/test/java/org/apache/flume/sink/mongodb/TestMongoDbSinkEmbedded.java new file mode 100644 index 0000000..cc4ffbf --- /dev/null +++ b/flume-mongodb-sink/src/test/java/org/apache/flume/sink/mongodb/TestMongoDbSinkEmbedded.java @@ -0,0 +1,164 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to you under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.flume.sink.mongodb; + +import static org.junit.Assert.assertEquals; + +import com.mongodb.client.MongoClient; +import com.mongodb.client.MongoClients; +import com.mongodb.client.MongoCollection; +import com.mongodb.client.model.IndexOptions; +import com.mongodb.client.model.Indexes; +import de.flapdoodle.embed.mongo.MongodExecutable; +import de.flapdoodle.embed.mongo.MongodProcess; +import de.flapdoodle.embed.mongo.MongodStarter; +import de.flapdoodle.embed.mongo.config.MongodConfig; +import de.flapdoodle.embed.mongo.config.Net; +import de.flapdoodle.embed.mongo.distribution.Version; +import de.flapdoodle.embed.process.runtime.Network; +import java.nio.charset.StandardCharsets; +import java.util.HashMap; +import org.apache.flume.Channel; +import org.apache.flume.Context; +import org.apache.flume.EventDeliveryException; +import org.apache.flume.Sink; +import org.apache.flume.Transaction; +import org.apache.flume.channel.MemoryChannel; +import org.apache.flume.conf.Configurables; +import org.apache.flume.event.EventBuilder; +import org.bson.Document; +import org.junit.AfterClass; +import org.junit.BeforeClass; +import org.junit.Test; + +/** + * Integration tests that exercise {@link MongoDbSink} against a real (but + * embedded/in-memory) MongoDB instance provided by Flapdoodle, verifying + * that duplicate key errors are tolerated instead of failing the whole + * batch and are tracked via a dedicated counter. + */ +public class TestMongoDbSinkEmbedded { + + private static MongodExecutable mongodExecutable; + private static MongoClient mongoClient; + private static int port; + + @BeforeClass + public static void startMongo() throws Exception { + port = Network.getFreeServerPort(); + MongodStarter starter = MongodStarter.getDefaultInstance(); + MongodConfig mongodConfig = MongodConfig.builder() + .version(Version.Main.V4_4) + .net(new Net(port, Network.localhostIsIPv6())) + .build(); + MongodExecutable executable = starter.prepare(mongodConfig); + MongodProcess process = executable.start(); + mongodExecutable = executable; + mongoClient = MongoClients.create("mongodb://localhost:" + port); + // Keep a reference so the process isn't garbage collected/stopped early. + assert process != null; + } + + @AfterClass + public static void stopMongo() { + if (mongoClient != null) { + mongoClient.close(); + } + if (mongodExecutable != null) { + mongodExecutable.stop(); + } + } + + private static Context baseContext(String database, String collection) { + Context context = new Context(); + context.put(MongoDbSinkConstants.CONNECTION_URI, "mongodb://localhost:" + port); + context.put(MongoDbSinkConstants.DATABASE_NAME, database); + context.put(MongoDbSinkConstants.COLLECTION, collection); + return context; + } + + private static MongoDbSink createAndStartSink(Context context) { + MongoDbSink sink = new MongoDbSink(); + Channel channel = new MemoryChannel(); + Configurables.configure(channel, new Context()); + sink.setChannel(channel); + channel.start(); + Configurables.configure(sink, context); + sink.start(); + return sink; + } + + private static void putEvent(Channel channel, String json) { + Transaction tx = channel.getTransaction(); + tx.begin(); + channel.put(EventBuilder.withBody(json.getBytes(StandardCharsets.UTF_8), new HashMap<>())); + tx.commit(); + tx.close(); + } + + @Test + public void testDuplicateKeyDoesNotFailBatchAndIsCountedSeparately() throws EventDeliveryException { + String database = "testDb1"; + String collectionName = "events"; + Context context = baseContext(database, collectionName); + MongoDbSink sink = createAndStartSink(context); + try { + MongoCollection<Document> collection = + mongoClient.getDatabase(database).getCollection(collectionName); + collection.createIndex(Indexes.ascending("uid"), new IndexOptions().unique(true)); + + Channel channel = sink.getChannel(); + // Two distinct events plus one that duplicates the first's unique key. + putEvent(channel, "{\"uid\":1,\"value\":\"a\"}"); + putEvent(channel, "{\"uid\":2,\"value\":\"b\"}"); + putEvent(channel, "{\"uid\":1,\"value\":\"c\"}"); + + Sink.Status status = sink.process(); + + assertEquals(Sink.Status.READY, status); + assertEquals(2, collection.countDocuments()); + assertEquals(1, sink.getDuplicateEventCount()); + } finally { + sink.stop(); + } + } + + @Test + public void testNoDuplicatesLeavesDuplicateCountAtZero() throws EventDeliveryException { + String database = "testDb2"; + String collectionName = "events"; + Context context = baseContext(database, collectionName); + MongoDbSink sink = createAndStartSink(context); + try { + MongoCollection<Document> collection = + mongoClient.getDatabase(database).getCollection(collectionName); + collection.createIndex(Indexes.ascending("uid"), new IndexOptions().unique(true)); + + Channel channel = sink.getChannel(); + putEvent(channel, "{\"uid\":1,\"value\":\"a\"}"); + putEvent(channel, "{\"uid\":2,\"value\":\"b\"}"); + + Sink.Status status = sink.process(); + + assertEquals(Sink.Status.READY, status); + assertEquals(2, collection.countDocuments()); + assertEquals(0, sink.getDuplicateEventCount()); + } finally { + sink.stop(); + } + } +}
