7.1 – Connector Development Model
Overview
This module introduces the Kafka Connect plugin model for developing custom connectors. By the end, you will understand:
- The Connector API and Task API.
- Connector and Task lifecycle methods.
- Configuration definitions (
ConfigDef). - Plugin isolation and classloading.
This module establishes the foundation for custom connector development. Modules 7.2-7.4 will dive into Source and Sink implementation details.
Why Develop a Custom Connector?
When to Build a Custom Connector
✅ Build a Custom Connector When:
- No existing connector supports your source/sink system.
- Existing connectors lack critical features for your use case.
- You need fine-grained control over data movement logic.
❌ Avoid Building a Custom Connector When:
- An existing connector already exists (check Confluent Hub).
- The integration can be achieved with a custom producer/consumer + SMTs.
Cost-Benefit: Developing a connector requires upfront investment (implementation, testing, maintenance), but yields long-term benefits (reusability, operational simplicity).
Kafka Connect Plugin Architecture
Key Components
- Connector: Defines the job, manages configuration, and creates tasks.
- Task: Executes the actual data movement (read from source, write to sink).
- ConfigDef: Declares configuration properties and validation rules.
Separation of Concerns
Connector (Management Layer)
├── Validates configuration
├── Splits work into tasks
└── Monitors task health
Task (Execution Layer)
├── Reads/writes data
├── Commits offsets
└── Reports progress
Key Insight: Connectors are stateless—they don't process data directly. Tasks are stateful and do the actual work.
The Connector API
Interface: org.apache.kafka.connect.connector.Connector
All connectors extend either:
SourceConnector: For source connectors (external system → Kafka).SinkConnector: For sink connectors (Kafka → external system).
Key Methods
1. start(Map<String, String> props)
Purpose: Initialize the connector with configuration properties.
Called: Once, when the connector is created.
Responsibilities:
- Validate configuration.
- Initialize connection to external system (if needed).
- Store configuration for later use.
Example:
@Override
public void start(Map<String, String> props) {
this.config = new MyConnectorConfig(props);
// Validate connection to external system
try {
testConnection(config);
} catch (Exception e) {
throw new ConnectException("Failed to connect to external system", e);
}
}
2. Class<? extends Task> taskClass()
Purpose: Return the task class that will execute data movement.
Called: When the connector needs to create tasks.
Example:
@Override
public Class<? extends Task> taskClass() {
return MySourceTask.class;
}
3. List<Map<String, String>> taskConfigs(int maxTasks)
Purpose: Split work into multiple tasks by generating task-specific configurations.
Called: When the connector is started or reconfigured.
Parameters:
maxTasks: Maximum number of tasks (fromtasks.maxconfig).
Returns: List of configuration maps (one per task).
Example (JDBC Source: one task per table):
@Override
public List<Map<String, String>> taskConfigs(int maxTasks) {
List<String> tables = config.getTables(); // e.g., ["users", "orders", "products"]
List<Map<String, String>> taskConfigs = new ArrayList<>();
for (int i = 0; i < Math.min(maxTasks, tables.size()); i++) {
Map<String, String> taskConfig = new HashMap<>(config.originalsStrings());
taskConfig.put("table.name", tables.get(i));
taskConfigs.add(taskConfig);
}
return taskConfigs;
}
Result: If maxTasks=3 and tables=["users", "orders", "products"], create 3 tasks:
- Task 0:
{"table.name": "users"} - Task 1:
{"table.name": "orders"} - Task 2:
{"table.name": "products"}
4. void stop()
Purpose: Clean up resources when the connector is stopped.
Called: When the connector is deleted or the worker shuts down.
Example:
@Override
public void stop() {
// Close database connections, HTTP clients, etc.
if (connection != null) {
connection.close();
}
}
5. ConfigDef config()
Purpose: Define connector configuration properties (name, type, default, validation).
Called: When validating connector configuration via REST API.
Example:
@Override
public ConfigDef config() {
return new ConfigDef()
.define("connection.url", ConfigDef.Type.STRING, ConfigDef.NO_DEFAULT_VALUE,
ConfigDef.Importance.HIGH, "Database connection URL")
.define("table.whitelist", ConfigDef.Type.LIST, Collections.emptyList(),
ConfigDef.Importance.MEDIUM, "Tables to monitor")
.define("poll.interval.ms", ConfigDef.Type.INT, 5000,
ConfigDef.Range.atLeast(100), ConfigDef.Importance.LOW,
"Polling interval in milliseconds");
}
6. String version()
Purpose: Return the connector version (for logging and monitoring).
Example:
@Override
public String version() {
return "1.0.0";
}
The Task API
Interface: org.apache.kafka.connect.connector.Task
All tasks extend either:
SourceTask: For source connectors (poll external system, produce to Kafka).SinkTask: For sink connectors (consume from Kafka, write to external system).
Key Methods (Common to Source and Sink)
1. void start(Map<String, String> props)
Purpose: Initialize the task with task-specific configuration.
Called: Once, when the task is started.
Responsibilities:
- Parse task configuration.
- Connect to external system.
- Initialize state (e.g., last offset read).
Example:
@Override
public void start(Map<String, String> props) {
this.config = new MyTaskConfig(props);
this.connection = connectToExternalSystem(config);
this.lastOffset = loadLastOffset();
}
2. void stop()
Purpose: Clean up resources when the task is stopped.
Called: When the task is reassigned, the connector is deleted, or the worker shuts down.
Example:
@Override
public void stop() {
if (connection != null) {
connection.close();
}
}
3. String version()
Purpose: Return the task version (must match connector version).
Example:
@Override
public String version() {
return "1.0.0";
}
Configuration Definitions (ConfigDef)
Purpose
ConfigDef defines:
- Configuration property names.
- Data types (STRING, INT, BOOLEAN, LIST, etc.).
- Default values.
- Validation rules (e.g., range, regex, custom validators).
- Documentation (displayed in REST API validation).
Example: Full ConfigDef
public static ConfigDef configDef() {
return new ConfigDef()
// Required property
.define("connection.url", ConfigDef.Type.STRING, ConfigDef.NO_DEFAULT_VALUE,
ConfigDef.Importance.HIGH, "Database connection URL")
// Optional property with default
.define("poll.interval.ms", ConfigDef.Type.INT, 5000,
ConfigDef.Range.atLeast(100), ConfigDef.Importance.MEDIUM,
"Polling interval in milliseconds")
// List property
.define("table.whitelist", ConfigDef.Type.LIST, Collections.emptyList(),
ConfigDef.Importance.HIGH, "Tables to monitor (comma-separated)")
// Boolean property
.define("auto.create", ConfigDef.Type.BOOLEAN, false,
ConfigDef.Importance.LOW, "Auto-create missing topics")
// Custom validator
.define("batch.size", ConfigDef.Type.INT, 1000,
new ConfigDef.Range(1, 10000), ConfigDef.Importance.MEDIUM,
"Batch size (1-10000)");
}
Property Importance Levels
| Importance | Description |
|---|---|
| HIGH | Required or critical properties (e.g., connection URL) |
| MEDIUM | Important but optional properties (e.g., batch size) |
| LOW | Advanced or rarely changed properties (e.g., timeouts) |
Plugin Isolation and Classloading
What is Plugin Isolation?
Plugin isolation ensures that connector JARs (and their dependencies) are loaded in isolated classloaders, preventing dependency conflicts.
Example: If Connector A uses Jackson 2.10 and Connector B uses Jackson 2.12, both can coexist without conflicts.
Plugin Path
Configuration: plugin.path in worker config.
Purpose: Directory where connector JARs are located.
Example:
plugin.path=/usr/local/share/kafka/plugins
Directory Structure:
/usr/local/share/kafka/plugins/
├── jdbc-connector/
│ ├── kafka-connect-jdbc-10.5.0.jar
│ └── postgresql-42.3.1.jar
└── elasticsearch-connector/
├── kafka-connect-elasticsearch-11.1.0.jar
└── elasticsearch-rest-client-7.15.0.jar
Key Rule: Each connector should be in its own subdirectory to enable plugin isolation.
Connector Lifecycle
1. Deployment
Operator submits connector configuration via REST API:
curl -X POST http://localhost:8083/connectors \
-H "Content-Type: application/json" \
-d '{"name": "my-connector", "config": {...}}'
2. Connector Initialization
- Connect worker validates configuration using
ConfigDef. - Worker calls
Connector.start(props)to initialize the connector. - Connector validates connection to external system.
3. Task Creation
- Worker calls
Connector.taskConfigs(maxTasks)to generate task configurations. - Worker assigns tasks to workers (in distributed mode).
4. Task Execution
- Worker calls
Task.start(props)for each task. - Tasks begin processing data (source:
poll(), sink:put()).
5. Rebalancing
If workers join/leave or tasks.max changes:
- Worker calls
Task.stop()for reassigned tasks. - Worker calls
Connector.taskConfigs(maxTasks)to recompute task assignments. - Worker calls
Task.start(props)for newly assigned tasks.
6. Connector Deletion
- Worker calls
Task.stop()for all tasks. - Worker calls
Connector.stop()to clean up resources.
Example: Minimal Source Connector
Connector Class
public class MySourceConnector extends SourceConnector {
private Map<String, String> config;
@Override
public void start(Map<String, String> props) {
this.config = props;
}
@Override
public Class<? extends Task> taskClass() {
return MySourceTask.class;
}
@Override
public List<Map<String, String>> taskConfigs(int maxTasks) {
List<Map<String, String>> configs = new ArrayList<>();
for (int i = 0; i < maxTasks; i++) {
configs.add(new HashMap<>(config));
}
return configs;
}
@Override
public void stop() {
// Cleanup
}
@Override
public ConfigDef config() {
return new ConfigDef()
.define("topic", ConfigDef.Type.STRING, ConfigDef.NO_DEFAULT_VALUE,
ConfigDef.Importance.HIGH, "Target Kafka topic");
}
@Override
public String version() {
return "1.0.0";
}
}
Key Takeaways
- Connector API: Manages configuration, splits work, and creates tasks (
start,taskConfigs,stop). - Task API: Executes data movement (
start,poll/put,stop). - ConfigDef: Declares configuration properties, types, defaults, and validation.
- Plugin Isolation: Connectors are loaded in isolated classloaders to prevent dependency conflicts.
- Lifecycle: Deployment → Connector Init → Task Creation → Execution → Rebalancing → Deletion.
What's Next?
- Module 7.2 covers Source Connector implementation in detail.
- Module 7.3 covers Sink Connector implementation in detail.
- Module 7.4 explains testing and packaging custom connectors.
Further Reading
End of Module 7.1