Skip to content

Commit cb8d95a

Browse files
authored
Merge pull request #563 from microsoft/dev
Release1.14.2
2 parents 3835a15 + edf8d2a commit cb8d95a

File tree

8 files changed

+179
-87
lines changed

8 files changed

+179
-87
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
## Release History
22

3+
### 1.14.2 (2024-03-12)
4+
#### Key Bug Fixes
5+
* Fixed an issue where only 1 task run successfully when `CosmosDBSourceConnector` is configured with `maxTasks` larger than `1` - [PR 561](https://github.com/microsoft/kafka-connect-cosmosdb/pull/561)
6+
37
### 1.14.1 (2024-02-29)
48
#### Key Bug Fixes
59
* Fixed `NullPointerException` in `CosmosDBSourceConnector`. [PR 555](https://github.com/microsoft/kafka-connect-cosmosdb/pull/555)

pom.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77

88
<groupId>com.azure.cosmos.kafka</groupId>
99
<artifactId>kafka-connect-cosmos</artifactId>
10-
<version>1.14.1</version>
10+
<version>1.14.2</version>
1111

1212
<name> kafka-connect-cosmos</name>
1313
<url>https://github.com/microsoft/kafka-connect-cosmosdb</url>
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
package com.azure.cosmos.kafka.connect.implementations;
5+
6+
import com.azure.cosmos.ConsistencyLevel;
7+
import com.azure.cosmos.CosmosAsyncClient;
8+
import com.azure.cosmos.CosmosClientBuilder;
9+
import com.azure.cosmos.kafka.connect.source.CosmosDBSourceConfig;
10+
import org.apache.commons.lang3.StringUtils;
11+
12+
import static com.azure.cosmos.implementation.guava25.base.Preconditions.checkArgument;
13+
14+
public class CosmosClientStore {
15+
public static CosmosAsyncClient getCosmosClient(CosmosDBSourceConfig config, String userAgentSuffix) {
16+
checkArgument(StringUtils.isNotEmpty(userAgentSuffix), "Argument 'userAgentSuffix' can not be null");
17+
18+
CosmosClientBuilder cosmosClientBuilder = new CosmosClientBuilder()
19+
.endpoint(config.getConnEndpoint())
20+
.key(config.getConnKey())
21+
.consistencyLevel(ConsistencyLevel.SESSION)
22+
.contentResponseOnWriteEnabled(true)
23+
.connectionSharingAcrossClientsEnabled(config.isConnectionSharingEnabled())
24+
.userAgentSuffix(userAgentSuffix);
25+
26+
if (config.isGatewayModeEnabled()) {
27+
cosmosClientBuilder.gatewayMode();
28+
}
29+
30+
return cosmosClientBuilder.buildAsyncClient();
31+
}
32+
}

src/main/java/com/azure/cosmos/kafka/connect/source/CosmosDBSourceConfig.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ public class CosmosDBSourceConfig extends CosmosDBConfig {
6464
private static final String COSMOS_USE_LATEST_OFFSET_DISPLAY = "Use latest offset";
6565

6666
static final String COSMOS_ASSIGNED_CONTAINER_CONF = "connect.cosmos.assigned.container";
67+
static final String COSMOS_ASSIGNED_LEASE_CONTAINER_CONF = "connect.cosmos.assigned.lease.container";
6768

6869
static final String COSMOS_WORKER_NAME_CONF = "connect.cosmos.worker.name";
6970
static final String COSMOS_WORKER_NAME_DEFAULT = "worker";
@@ -80,6 +81,7 @@ public class CosmosDBSourceConfig extends CosmosDBConfig {
8081
// Variables not defined as Connect configs, should not be exposed when creating connector
8182
private String workerName;
8283
private String assignedContainer;
84+
private String assignedLeaseContainer;
8385

8486
public CosmosDBSourceConfig(ConfigDef config, Map<String, String> parsedConfig) {
8587
super(config, parsedConfig);
@@ -98,6 +100,7 @@ public CosmosDBSourceConfig(Map<String, String> parsedConfig) {
98100

99101
// Since variables are not defined as Connect configs, grab values directly from Map
100102
assignedContainer = parsedConfig.get(COSMOS_ASSIGNED_CONTAINER_CONF);
103+
assignedLeaseContainer = parsedConfig.get(COSMOS_ASSIGNED_LEASE_CONTAINER_CONF);
101104
workerName = parsedConfig.get(COSMOS_WORKER_NAME_CONF);
102105
}
103106

@@ -242,6 +245,10 @@ public String getAssignedContainer() {
242245
return this.assignedContainer;
243246
}
244247

248+
public String getAssignedLeaseContainer() {
249+
return assignedLeaseContainer;
250+
}
251+
245252
public String getWorkerName() {
246253
return this.workerName;
247254
}

src/main/java/com/azure/cosmos/kafka/connect/source/CosmosDBSourceConnector.java

Lines changed: 68 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,17 @@
88

99
import java.util.function.Function;
1010
import java.util.stream.Collectors;
11+
12+
import com.azure.cosmos.CosmosAsyncClient;
13+
import com.azure.cosmos.CosmosAsyncContainer;
14+
import com.azure.cosmos.CosmosAsyncDatabase;
15+
import com.azure.cosmos.CosmosException;
16+
import com.azure.cosmos.kafka.connect.CosmosDBConfig;
17+
import com.azure.cosmos.kafka.connect.implementations.CosmosClientStore;
18+
import com.azure.cosmos.models.CosmosContainerProperties;
19+
import com.azure.cosmos.models.CosmosContainerRequestOptions;
20+
import com.azure.cosmos.models.CosmosContainerResponse;
21+
import com.azure.cosmos.models.ThroughputProperties;
1122
import org.apache.commons.lang3.RandomUtils;
1223
import org.apache.kafka.common.config.Config;
1324
import org.apache.kafka.common.config.ConfigDef;
@@ -28,12 +39,20 @@ public class CosmosDBSourceConnector extends SourceConnector {
2839

2940
private static final Logger logger = LoggerFactory.getLogger(CosmosDBSourceConnector.class);
3041
private CosmosDBSourceConfig config = null;
42+
private CosmosAsyncClient cosmosClient = null;
3143

3244
@Override
3345
public void start(Map<String, String> props) {
3446
logger.info("Starting the Source Connector");
3547
try {
3648
config = new CosmosDBSourceConfig(props);
49+
this.cosmosClient = CosmosClientStore.getCosmosClient(this.config, this.getUserAgentSuffix());
50+
51+
List<String> containerList = config.getTopicContainerMap().getContainerList();
52+
for (String containerId : containerList) {
53+
createLeaseContainerIfNotExists(cosmosClient, this.config.getDatabaseName(), this.getAssignedLeaseContainer(containerId));
54+
}
55+
3756
} catch (ConfigException e) {
3857
throw new ConnectException(
3958
"Couldn't start CosmosDBSourceConnector due to configuration error", e);
@@ -59,8 +78,10 @@ public List<Map<String, String>> taskConfigs(int maxTasks) {
5978
for (int i = 0; i < maxTasks; i++) {
6079
// Equally distribute workers by assigning workers to containers in round-robin fashion.
6180
Map<String, String> taskProps = config.originalsStrings();
62-
taskProps.put(CosmosDBSourceConfig.COSMOS_ASSIGNED_CONTAINER_CONF,
63-
containerList.get(i % containerList.size()));
81+
String assignedContainer = containerList.get(i % containerList.size());
82+
83+
taskProps.put(CosmosDBSourceConfig.COSMOS_ASSIGNED_CONTAINER_CONF, assignedContainer);
84+
taskProps.put(CosmosDBSourceConfig.COSMOS_ASSIGNED_LEASE_CONTAINER_CONF, this.getAssignedLeaseContainer(assignedContainer));
6485
taskProps.put(CosmosDBSourceConfig.COSMOS_WORKER_NAME_CONF,
6586
String.format("%s-%d-%d",
6687
CosmosDBSourceConfig.COSMOS_WORKER_NAME_DEFAULT,
@@ -74,6 +95,9 @@ public List<Map<String, String>> taskConfigs(int maxTasks) {
7495
@Override
7596
public void stop() {
7697
logger.info("Stopping CosmosDB Source Connector");
98+
if (this.cosmosClient != null) {
99+
this.cosmosClient.close();
100+
}
77101
}
78102

79103
@Override
@@ -101,4 +125,46 @@ public Config validate(Map<String, String> connectorConfigs) {
101125

102126
return config;
103127
}
128+
129+
private String getAssignedLeaseContainer(String containerName) {
130+
return containerName + "-leases";
131+
}
132+
133+
private String getUserAgentSuffix() {
134+
return CosmosDBConfig.COSMOS_CLIENT_USER_AGENT_SUFFIX + version();
135+
}
136+
137+
private CosmosAsyncContainer createLeaseContainerIfNotExists(CosmosAsyncClient client, String databaseName, String leaseCollectionName) {
138+
CosmosAsyncDatabase database = client.getDatabase(databaseName);
139+
CosmosAsyncContainer leaseCollection = database.getContainer(leaseCollectionName);
140+
CosmosContainerResponse leaseContainerResponse = null;
141+
142+
logger.info("Checking whether the lease container exists.");
143+
try {
144+
leaseContainerResponse = leaseCollection.read().block();
145+
} catch (CosmosException ex) {
146+
// Swallowing exceptions when the type is CosmosException and statusCode is 404
147+
if (ex.getStatusCode() != 404) {
148+
throw ex;
149+
}
150+
logger.info("Lease container does not exist {}", ex.getMessage());
151+
}
152+
153+
if (leaseContainerResponse == null) {
154+
logger.info("Creating the Lease container : {}", leaseCollectionName);
155+
CosmosContainerProperties containerSettings = new CosmosContainerProperties(leaseCollectionName, "/id");
156+
ThroughputProperties throughputProperties = ThroughputProperties.createManualThroughput(400);
157+
CosmosContainerRequestOptions requestOptions = new CosmosContainerRequestOptions();
158+
159+
try {
160+
database.createContainer(containerSettings, throughputProperties, requestOptions).block();
161+
} catch (Exception e) {
162+
logger.error("Failed to create container {} in database {}", leaseCollectionName, databaseName);
163+
throw e;
164+
}
165+
logger.info("Successfully created new lease container.");
166+
}
167+
168+
return database.getContainer(leaseCollectionName);
169+
}
104170
}

src/main/java/com/azure/cosmos/kafka/connect/source/CosmosDBSourceTask.java

Lines changed: 23 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -5,27 +5,22 @@
55

66
import com.azure.cosmos.ChangeFeedProcessor;
77
import com.azure.cosmos.ChangeFeedProcessorBuilder;
8-
import com.azure.cosmos.ConsistencyLevel;
98
import com.azure.cosmos.CosmosAsyncClient;
109
import com.azure.cosmos.CosmosAsyncContainer;
1110
import com.azure.cosmos.CosmosAsyncDatabase;
12-
import com.azure.cosmos.CosmosClientBuilder;
13-
import com.azure.cosmos.CosmosException;
1411
import com.azure.cosmos.kafka.connect.CosmosDBConfig;
1512
import com.azure.cosmos.kafka.connect.TopicContainerMap;
13+
import com.azure.cosmos.kafka.connect.implementations.CosmosClientStore;
1614
import com.azure.cosmos.kafka.connect.implementations.CosmosKafkaSchedulers;
1715
import com.azure.cosmos.models.ChangeFeedProcessorOptions;
18-
import com.azure.cosmos.models.CosmosContainerProperties;
19-
import com.azure.cosmos.models.CosmosContainerRequestOptions;
20-
import com.azure.cosmos.models.CosmosContainerResponse;
21-
import com.azure.cosmos.models.ThroughputProperties;
2216
import com.fasterxml.jackson.databind.JsonNode;
2317
import org.apache.kafka.connect.data.Schema;
2418
import org.apache.kafka.connect.data.SchemaAndValue;
2519
import org.apache.kafka.connect.source.SourceRecord;
2620
import org.apache.kafka.connect.source.SourceTask;
2721
import org.slf4j.Logger;
2822
import org.slf4j.LoggerFactory;
23+
import reactor.core.publisher.Mono;
2924

3025
import java.time.Duration;
3126
import java.util.ArrayList;
@@ -69,13 +64,13 @@ public void start(Map<String, String> map) {
6964
this.queue = new LinkedTransferQueue<>();
7065

7166
logger.info("Worker {} Creating the client.", this.config.getWorkerName());
72-
client = getCosmosClient(config);
67+
client = CosmosClientStore.getCosmosClient(this.config, this.getUserAgentSuffix());
7368

7469
// Initialize the database, feed and lease containers
7570
CosmosAsyncDatabase database = client.getDatabase(config.getDatabaseName());
7671
String container = config.getAssignedContainer();
7772
CosmosAsyncContainer feedContainer = database.getContainer(container);
78-
leaseContainer = createNewLeaseContainer(client, config.getDatabaseName(), container + "-leases");
73+
leaseContainer = database.getContainer(this.config.getAssignedLeaseContainer());
7974

8075
// Create source partition map
8176
partitionMap = new HashMap<>();
@@ -212,34 +207,28 @@ public void stop() {
212207
// NOTE: poll() method and stop() method are both called from the same thread,
213208
// so it is important not to include any changes which may block both places forever
214209
running.set(false);
215-
216-
// Release all the resources.
217-
if (changeFeedProcessor != null) {
218-
changeFeedProcessor.stop().block();
219-
changeFeedProcessor = null;
220-
}
221210

222-
if (this.client != null) {
223-
this.client.close();
224-
}
211+
Mono.just(this)
212+
.flatMap(connectorTask -> {
213+
if (this.changeFeedProcessor != null) {
214+
return this.changeFeedProcessor.stop()
215+
.delayElement(Duration.ofMillis(500)) // delay some time here as the partitionProcessor will release the lease in background
216+
.doOnNext(t -> {
217+
this.changeFeedProcessor = null;
218+
this.safeCloseClient();
219+
});
220+
} else {
221+
this.safeCloseClient();
222+
return Mono.empty();
223+
}
224+
})
225+
.block();
225226
}
226227

227-
private CosmosAsyncClient getCosmosClient(CosmosDBSourceConfig config) {
228-
logger.info("Worker {} Creating Cosmos Client.", this.config.getWorkerName());
229-
230-
CosmosClientBuilder cosmosClientBuilder = new CosmosClientBuilder()
231-
.endpoint(config.getConnEndpoint())
232-
.key(config.getConnKey())
233-
.consistencyLevel(ConsistencyLevel.SESSION)
234-
.contentResponseOnWriteEnabled(true)
235-
.connectionSharingAcrossClientsEnabled(config.isConnectionSharingEnabled())
236-
.userAgentSuffix(getUserAgentSuffix());
237-
238-
if (config.isGatewayModeEnabled()) {
239-
cosmosClientBuilder.gatewayMode();
228+
private void safeCloseClient() {
229+
if (this.client != null) {
230+
this.client.close();
240231
}
241-
242-
return cosmosClientBuilder.buildAsyncClient();
243232
}
244233

245234
private String getUserAgentSuffix() {
@@ -292,6 +281,7 @@ protected void handleCosmosDbChanges(List<JsonNode> docs) {
292281
// The queue is being continuously polled and then put into a batch list, but the batch list is not being flushed right away
293282
// until batch size or maxWaitTime reached. Which can cause CFP to checkpoint faster than kafka batch.
294283
// In order to not move CFP checkpoint faster, we are using shouldFillMoreRecords to control the batch flush.
284+
logger.debug("Transferring document " + this.config.getWorkerName());
295285
this.queue.transfer(document);
296286
} catch (InterruptedException e) {
297287
logger.error("Interrupted! changeFeedReader.", e);
@@ -307,38 +297,4 @@ protected void handleCosmosDbChanges(List<JsonNode> docs) {
307297
this.shouldFillMoreRecords.set(false);
308298
}
309299
}
310-
311-
private CosmosAsyncContainer createNewLeaseContainer(CosmosAsyncClient client, String databaseName, String leaseCollectionName) {
312-
CosmosAsyncDatabase database = client.getDatabase(databaseName);
313-
CosmosAsyncContainer leaseCollection = database.getContainer(leaseCollectionName);
314-
CosmosContainerResponse leaseContainerResponse = null;
315-
316-
logger.info("Checking whether the lease container exists.");
317-
try {
318-
leaseContainerResponse = leaseCollection.read().block();
319-
} catch (CosmosException ex) {
320-
// Swallowing exceptions when the type is CosmosException and statusCode is 404
321-
if (ex.getStatusCode() != 404) {
322-
throw ex;
323-
}
324-
logger.info("Lease container does not exist {}", ex.getMessage());
325-
}
326-
327-
if (leaseContainerResponse == null) {
328-
logger.info("Creating the Lease container : {}", leaseCollectionName);
329-
CosmosContainerProperties containerSettings = new CosmosContainerProperties(leaseCollectionName, "/id");
330-
ThroughputProperties throughputProperties = ThroughputProperties.createManualThroughput(400);
331-
CosmosContainerRequestOptions requestOptions = new CosmosContainerRequestOptions();
332-
333-
try {
334-
database.createContainer(containerSettings, throughputProperties, requestOptions).block();
335-
} catch (Exception e) {
336-
logger.error("Failed to create container {} in database {}", leaseCollectionName, databaseName);
337-
throw e;
338-
}
339-
logger.info("Successfully created new lease container.");
340-
}
341-
342-
return database.getContainer(leaseCollectionName);
343-
}
344300
}

0 commit comments

Comments
 (0)