[FLINK-39732] Introduce TableDiscoverer SPI for flexible table subscription (with default JdbcTableDiscoverer) - #4409
Conversation
…iption (with default JdbcTableDiscoverer)
|
@leonardBang , CC |
…iption (with default JdbcTableDiscoverer)
7d3ee3a to
8903047
Compare
There was a problem hiding this comment.
Pull request overview
Introduces a new, connector-agnostic TableDiscoverer SPI (loaded via ServiceLoader) to decouple “which tables to subscribe to” logic from individual CDC connectors, and ships a default JDBC-based implementation plus integration tests.
Changes:
- Add
TableDiscovererandTableDiscovererFactorySPI types with ServiceLoader-based resolution. - Provide the default
JdbcTableDiscoverer+JdbcTableDiscovererFactoryimplementation. - Add a MySQL Testcontainers IT case and Maven test dependencies to validate JDBC modes and SPI loading.
Reviewed changes
Copilot reviewed 8 out of 8 changed files in this pull request and generated 10 comments.
Show a summary per file
| File | Description |
|---|---|
| flink-cdc-common/src/main/java/org/apache/flink/cdc/common/source/discover/TableDiscoverer.java | Defines the public SPI interface and lifecycle for table discovery. |
| flink-cdc-common/src/main/java/org/apache/flink/cdc/common/source/discover/TableDiscovererFactory.java | Implements SPI factory lookup and a default Context helper. |
| flink-cdc-common/src/main/java/org/apache/flink/cdc/common/source/discover/JdbcTableDiscoverer.java | Adds the default JDBC-backed discoverer (shared-table mode + custom-query mode). |
| flink-cdc-common/src/main/java/org/apache/flink/cdc/common/source/discover/JdbcTableDiscovererFactory.java | Registers the JDBC discoverer under identifier jdbc. |
| flink-cdc-common/src/main/resources/META-INF/services/org.apache.flink.cdc.common.source.discover.TableDiscovererFactory | Adds ServiceLoader registration for the JDBC factory. |
| flink-cdc-common/src/test/java/org/apache/flink/cdc/common/source/JdbcTableDiscovererITCase.java | Adds integration tests validating SPI loading and both JDBC discovery modes. |
| flink-cdc-common/pom.xml | Adds test dependencies for MySQL Testcontainers-based integration testing. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| static TableDiscoverer.Context createContext( | ||
| Configuration configuration, ClassLoader classLoader) { | ||
| return new DefaultDiscovererContext(configuration, classLoader); | ||
| } |
| static TableDiscoverer createDiscoverer(String type, ClassLoader classLoader) { | ||
| ClassLoader loader = | ||
| classLoader != null ? classLoader : Thread.currentThread().getContextClassLoader(); | ||
| ServiceLoader<TableDiscovererFactory> serviceLoader = | ||
| ServiceLoader.load(TableDiscovererFactory.class, loader); | ||
|
|
| * <p>using a {@link PreparedStatement} (injection-safe), and only the rows whose subscribe-id | ||
| * matches the configured value are returned. |
| * <p>Null values and rows that cannot be parsed into a valid {@link TableId} are silently skipped. | ||
| */ |
| if (subscribeQuery != null && !subscribeQuery.isEmpty()) { | ||
| // Mode A — custom query takes priority. Filter options are intentionally ignored. | ||
| this.sql = subscribeQuery; |
| } else { | ||
| // Mode B — shared-table filter; subscribe-id is mandatory. | ||
| String tableName = requireNonEmpty(config, TABLE_NAME); |
| private static String requireNonEmpty(Configuration config, ConfigOption<String> option) { | ||
| String value = config.get(option); | ||
| if (value == null || value.isEmpty()) { | ||
| throw new IllegalArgumentException( | ||
| "'" + option.key() + "' is required for JdbcTableDiscoverer."); | ||
| } | ||
| return value; | ||
| } |
| } catch (IllegalArgumentException e) { | ||
| LOG.warn( | ||
| "Skipping invalid table name '{}' returned by JdbcTableDiscoverer.", value); | ||
| } |
| private static final MySQLContainer<?> MYSQL = | ||
| new MySQLContainer<>("mysql:8.0") | ||
| .withDatabaseName("meta_db") |
| <dependency> | ||
| <groupId>mysql</groupId> | ||
| <artifactId>mysql-connector-java</artifactId> | ||
| <version>8.0.27</version> | ||
| <scope>test</scope> | ||
| </dependency> |
| subscribeId); | ||
| } | ||
|
|
||
| connection = DriverManager.getConnection(jdbcUrl, username, password); |
There was a problem hiding this comment.
Context now exposes a user code class loader, but it is not used here when opening the JDBC connection. In deployments where the driver only exists in the user or connector classpath, this can still fail with No suitable driver. Please explicitly load or register the driver with that class loader before calling DriverManager.getConnection(...).
There was a problem hiding this comment.
This submodule deletion looks unrelated to the TableDiscoverer change and leaves the repo in an inconsistent state: .gitmodules still declares docs/themes/book, docs/README.md still asks contributors to initialize submodules, and .github/workflows/docs.sh still does so before building the docs. Please revert this deletion unless the full submodule cleanup is part of this PR.
| * META-INF/services/org.apache.flink.cdc.common.source.discover.TableDiscovererFactory | ||
| * }</pre> | ||
| * | ||
| * <p>The discoverer type is selected by the user via the {@code table.discoverer.type} option, |
There was a problem hiding this comment.
I could not find a production integration point in this PR that actually consumes table.discoverer.type or calls TableDiscovererFactory.createDiscoverer(...); the usages I found are only in tests. As written, this documentation implies the option is already wired into the real source or data source creation flow. Could you either add that integration here or narrow the documentation so it does not overstate the current functionality?
| package org.apache.flink.cdc.common.source.discover; | ||
|
|
||
| /** | ||
| * {@link TableDiscovererFactory} for {@link JdbcTableDiscoverer}. Activated when {@code |
There was a problem hiding this comment.
I grepped the PR branch and the only TableDiscovererFactory.createDiscoverer(...) call sites I could find are in JdbcTableDiscovererITCase; I could not find any src/main caller yet. Because of that, saying this is already activated by table.discoverer.type = 'jdbc' reads stronger than the current implementation. Could you either wire this option into the production source creation path in this PR or soften this Javadoc for now?
There was a problem hiding this comment.
I could not find any src/main caller yet.
I will use it in fluss yaml source later (poc code is here:https://github.com/loserwang1024/flink-cdc-connectors/tree/fluss-yaml-source).
Could you either wire this option into the production source creation path in this PR or soften this Javadoc for now?
I will soften this Javadoc for now.
| * @return A set of {@link TableId} representing the tables to read. | ||
| * @throws Exception if the discovery fails. | ||
| */ | ||
| Set<TableId> discover() throws Exception; |
There was a problem hiding this comment.
Can we make TableId generic here? Table ID might not be a suitable abstraction for multimodal data sources.
There was a problem hiding this comment.
Maybe rename TableDiscoverer => ObjectIdDiscoverer, too?
fix https://issues.apache.org/jira/browse/FLINK-39732
Summary
Today, every Flink CDC source connector hard-codes its own way of deciding which tables to subscribe to — typically a regex on table names, a hard-coded list, or a connector-specific include/exclude DSL. As Flink CDC grows beyond a single source family (MySQL, Postgres, Fluss, …), this duplication becomes painful:
This issue proposes a small, connector-agnostic, pluggable SPI —
TableDiscoverer— together with a default JDBC-backed implementation that covers the most common "subscription metadata table" pattern. The SPI is loaded via JavaServiceLoader, selected by a single config optiontable.discoverer.type, and shares a unifiedtable.discoverer.*configuration namespace.Motivation
The pluggable subscription mechanism aims to satisfy three demands at once:
A non-goal of this issue is to refactor existing source connectors to use the new SPI. That migration will happen incrementally in follow-up issues; this PR only introduces and ships the SPI plus a default implementation so that downstream connectors can opt in.
Public API
TableDiscovererLifecycle contract:
open(Context)is invoked exactly once before the firstdiscover().discover()may be called multiple times during the source lifetime; implementations are encouraged to be idempotent and fast.close()is called when the discoverer is no longer needed (job teardown, source restart) and must release I/O resources held inopen().TableDiscovererFactoryResolution rules:
ServiceLoader<TableDiscovererFactory>.table.discoverer.typeis matched case-insensitively againstidentifier().IllegalStateExceptionat startup (clear error message lists offending classes).IllegalArgumentExceptionlisting the set of known identifiers, which makes config typos easy to debug.Configuration namespace
All built-in and third-party discoverers are expected to use the unified prefix
table.discoverer.*. The single global key is:table.discoverer.typejdbc).Implementation-specific keys live under
table.discoverer.<identifier>.*.Default implementation:
JdbcTableDiscovererThe JDBC discoverer reads the subscription list from any JDBC-compatible database. It supports two modes, chosen implicitly by the configuration:
Mode A — Shared subscription table (default & recommended)
Designed for the common multi-tenant pattern where many CDC jobs share one metadata table and each job filters by a
subscribe-id.Effective query (built with a
PreparedStatement, injection-safe on the parameter):table.discoverer.jdbc.urltable.discoverer.jdbc.usernametable.discoverer.jdbc.passwordtable.discoverer.jdbc.table-nametable.discoverer.jdbc.subscribe-idtable.discoverer.jdbc.column-namesubscribe_table_nametable.discoverer.jdbc.subscribe-id-columnsubscribe_idWHEREclauseRecommended schema:
The legacy "unset
subscribe-id⇒ scan the whole metadata table" fallback has been intentionally removed: leavingsubscribe-idblank is now an error, which prevents silent over-subscription in shared deployments.Mode B — Custom SELECT (escape hatch)
For uncommon layouts (JOINs, soft filters, multi-column metadata) users may set:
table.discoverer.jdbc.subscribe-queryExample