diff --git a/conf/jvm.conf b/conf/jvm.conf
index fcc3742..86d3aa6 100644
--- a/conf/jvm.conf
+++ b/conf/jvm.conf
@@ -3,6 +3,8 @@
-Dfile.encoding=UTF-8
-Duser.timezone=UTC
+-Djava.util.logging.manager=org.apache.logging.log4j.jul.LogManager
+
-Xms8g
-Xmx60g
diff --git a/conf/pixels-sink.aws.properties b/conf/pixels-sink.aws.properties
index cd9f98d..a6ef8b3 100644
--- a/conf/pixels-sink.aws.properties
+++ b/conf/pixels-sink.aws.properties
@@ -1,5 +1,8 @@
# engine | kafka | storage
sink.datasource=storage
+sink.storage.mode=stream
+# CDC dialect for envelope normalization: mysql | postgresql
+sink.debezium.dialect=postgresql
# -1 means no limit, Only implement in retina sink mode yet:
sink.datasource.rate.limit=200000
sink.monitor.report.file=/home/ubuntu/pixels-sink/result1k2_feb/rate_8192tile_3.csv
@@ -20,11 +23,11 @@ sink.retina.trans.request.batch.size=100
sink.trans.mode=batch
sink.monitor.report.enable=true
# trino for freshness query
-trino.url=jdbc:trino://realtime-pixels-coordinator:8080/pixels/pixels_bench
-# trino.url=jdbc:trino://realtime-pixels-coordinator:8080/pixels/pixels_bench_sf10x
-trino.user=pixels
-trino.password=password
-trino.parallel=1
+sink.query.url=jdbc:trino://realtime-pixels-coordinator:8080/pixels/pixels_bench
+# sink.query.url=jdbc:trino://realtime-pixels-coordinator:8080/pixels/pixels_bench_sf10x
+sink.query.user=pixels
+sink.query.password=password
+sink.query.parallel=1
# row or txn or embed
sink.monitor.freshness.level=embed
sink.monitor.freshness.verbose=true
@@ -41,8 +44,7 @@ bootstrap.servers=realtime-kafka-2:29092
group.id=3078
auto.offset.reset=earliest
key.deserializer=org.apache.kafka.common.serialization.StringDeserializer
-#value.deserializer=io.pixelsdb.pixels.writer.deserializer.RowChangeEventAvroDeserializer
-value.deserializer=io.pixelsdb.pixels.sink.event.deserializer.RowChangeEventJsonDeserializer
+sink.kafka.value.format=json
# Topic & Database Config
topic.prefix=postgresql.oltp_server
consumer.capture_database=pixels_bench_sf1x
@@ -78,8 +80,6 @@ sink.flink.server.port=9091
sink.registry.url=http://localhost:8080/apis/registry/v2
# Transaction Config
transaction.topic.suffix=transaction
-#transaction.topic.value.deserializer=io.pixelsdb.pixels.writer.deserializer.TransactionAvroMessageDeserializer
-transaction.topic.value.deserializer=io.pixelsdb.pixels.sink.event.deserializer.TransactionJsonMessageDeserializer
sink.trans.batch.size=100
# Sink Metrics
@@ -90,7 +90,6 @@ sink.monitor.freshness.interval=1000
# Interact with other rpc
sink.rpc.enable=true
-sink.rpc.mock.delay=20
# debezium engine config
debezium.name=testEngine
debezium.connector.class=io.debezium.connector.postgresql.PostgresConnector
diff --git a/conf/pixels-sink.ch.properties b/conf/pixels-sink.ch.properties
index c9a4fbe..7312a01 100644
--- a/conf/pixels-sink.ch.properties
+++ b/conf/pixels-sink.ch.properties
@@ -1,5 +1,8 @@
# engine | kafka | storage
sink.datasource=storage
+sink.storage.mode=stream
+# CDC dialect for envelope normalization: mysql | postgresql
+sink.debezium.dialect=postgresql
sink.mode=retina
#sink.datasource=engine
#sink.mode=proto
@@ -23,10 +26,10 @@ sink.trans.mode=batch
# sink.trans.mode=record
sink.monitor.report.enable=true
# trino for freshness query
-trino.url=jdbc:trino://realtime-pixels-coordinator:8080/pixels/pixels_bench
-trino.user=pixels
-trino.password=password
-trino.parallel=1
+sink.query.url=jdbc:trino://realtime-pixels-coordinator:8080/pixels/pixels_bench
+sink.query.user=pixels
+sink.query.password=password
+sink.query.parallel=1
# row or txn or embed
sink.monitor.freshness.level=embed
sink.monitor.freshness.verbose=true
@@ -42,8 +45,7 @@ bootstrap.servers=realtime-kafka-2:29092
group.id=3078
auto.offset.reset=earliest
key.deserializer=org.apache.kafka.common.serialization.StringDeserializer
-#value.deserializer=io.pixelsdb.pixels.writer.deserializer.RowChangeEventAvroDeserializer
-value.deserializer=io.pixelsdb.pixels.sink.event.deserializer.RowChangeEventJsonDeserializer
+sink.kafka.value.format=json
# Topic & Database Config
topic.prefix=postgresql.oltp_server
consumer.capture_database=pixels_bench_sf1x
@@ -75,8 +77,6 @@ sink.flink.server.port=9091
sink.registry.url=http://localhost:8080/apis/registry/v2
# Transaction Config
transaction.topic.suffix=transaction
-#transaction.topic.value.deserializer=io.pixelsdb.pixels.writer.deserializer.TransactionAvroMessageDeserializer
-transaction.topic.value.deserializer=io.pixelsdb.pixels.sink.event.deserializer.TransactionJsonMessageDeserializer
sink.trans.batch.size=100
# Sink Metrics
@@ -87,7 +87,6 @@ sink.monitor.freshness.interval=1000
# Interact with other rpc
sink.rpc.enable=true
-sink.rpc.mock.delay=20
# debezium engine config
debezium.name=testEngine
debezium.connector.class=io.debezium.connector.postgresql.PostgresConnector
diff --git a/conf/pixels-sink.flink.properties.chbench b/conf/pixels-sink.flink.properties.chbench
index 6c87406..c862997 100644
--- a/conf/pixels-sink.flink.properties.chbench
+++ b/conf/pixels-sink.flink.properties.chbench
@@ -1,5 +1,6 @@
# engine | kafka | storage
sink.datasource=storage
+sink.storage.mode=stream
# -1 means no limit, Only implement in retina sink mode yet
sink.datasource.rate.limit=2000
# Sink Config: retina | csv | proto | flink | none
@@ -11,10 +12,10 @@ sink.monitor.report.enable=true
sink.monitor.report.file=/home/ubuntu/pixels-sink/result_delta/rate_test.csv
sink.monitor.freshness.file=/home/ubuntu/pixels-sink/result_delta/fresh_batch10k.csv
# trino for freshness query
-trino.url=jdbc:trino://realtime-pixels-coordinator:8080/delta_lake/chbenchmark_w10000
-trino.user=pixels
-trino.password=password
-trino.parallel=1
+sink.query.url=jdbc:trino://realtime-pixels-coordinator:8080/delta_lake/chbenchmark_w10000
+sink.query.user=pixels
+sink.query.password=password
+sink.query.parallel=1
# row or txn or embed
sink.monitor.freshness.level=embed
sink.monitor.freshness.embed.warmup=10
diff --git a/conf/pixels-sink.flink.properties.hybench b/conf/pixels-sink.flink.properties.hybench
index eb03619..d809dc6 100644
--- a/conf/pixels-sink.flink.properties.hybench
+++ b/conf/pixels-sink.flink.properties.hybench
@@ -1,5 +1,6 @@
# engine | kafka | storage
sink.datasource=storage
+sink.storage.mode=stream
# -1 means no limit, Only implement in retina sink mode yet
sink.datasource.rate.limit=500
sink.datasource.rate.limit.type=guava
@@ -12,10 +13,10 @@ sink.monitor.report.enable=true
sink.monitor.report.file=/home/ubuntu/pixels-sink/result_delta/rate_hybench_rate500.csv
sink.monitor.freshness.file=/home/ubuntu/pixels-sink/result_delta/fresh_hybench_rate500.csv
# trino for freshness query
-trino.url=jdbc:trino://realtime-pixels-coordinator:8080/delta_lake/hybench_sf1000
-trino.user=pixels
-trino.password=password
-trino.parallel=1
+sink.query.url=jdbc:trino://realtime-pixels-coordinator:8080/delta_lake/hybench_sf1000
+sink.query.user=pixels
+sink.query.password=password
+sink.query.parallel=1
# row or txn or embed
sink.monitor.freshness.level=embed
sink.monitor.freshness.embed.warmup=10
diff --git a/conf/pixels-sink.hudi.properties b/conf/pixels-sink.hudi.properties
index 8ab4983..801922c 100644
--- a/conf/pixels-sink.hudi.properties
+++ b/conf/pixels-sink.hudi.properties
@@ -1,5 +1,6 @@
# engine | kafka | storage
sink.datasource=storage
+sink.storage.mode=stream
# -1 means no limit, Only implement in retina sink mode yet
sink.datasource.rate.limit=2000
# Sink Config: retina | csv | proto | flink | none
@@ -10,11 +11,11 @@ sink.trans.mode=batch
sink.monitor.report.enable=true
sink.monitor.report.file=/home/ubuntu/pixels-sink/result_delta/rate_test.csv
sink.monitor.freshness.file=/home/ubuntu/pixels-sink/result_delta/fresh_batch10k.csv
-# trino for freshness query
-trino.url=jdbc:hive2://172.31.16.214:10000/hudi_hybench_sf1333_2
-trino.user=pixels
-trino.password=
-trino.parallel=1
+# HiveServer2 for Hudi RO incremental freshness (requires: mvn -Phudi-hive package)
+sink.query.url=jdbc:hive2://172.31.16.214:10000/hudi_hybench_sf1333_2
+sink.query.user=pixels
+sink.query.password=
+sink.query.parallel=1
# row or txn or embed
sink.monitor.freshness.level=embed
sink.monitor.freshness.embed.warmup=0
diff --git a/conf/pixels-sink.mysql.properties b/conf/pixels-sink.mysql.properties
new file mode 100644
index 0000000..58e5e1e
--- /dev/null
+++ b/conf/pixels-sink.mysql.properties
@@ -0,0 +1,102 @@
+# engine | kafka | storage
+sink.datasource=engine
+sink.datasource.engine.format=connect
+# CDC dialect for envelope normalization: mysql | postgresql
+sink.debezium.dialect=mysql
+sink.storage.mode=stream
+# -1 means no limit, Only implement in retina sink mode yet
+sink.datasource.rate.limit=100000
+# Sink Config: retina | csv | proto | flink | none
+sink.mode=retina
+sink.retina.client=8
+sink.retina.log.queue=false
+## batch or single or record, batch is recommend. record is faster, but doesn't have ACID feature
+sink.trans.mode=batch
+sink.monitor.report.enable=true
+sink.monitor.report.file=/home/ubuntu/pixels-sink/resulti7i/100k_rate_mysql.csv
+sink.monitor.freshness.file=/home/ubuntu/pixels-sink/resulti7i/100k_freshness_mysql.csv
+# trino for freshness query
+sink.query.url=jdbc:trino://realtime-kafka-2:8080/pixels/pixels_bench_sf10x
+sink.query.user=pixels
+sink.query.password=password
+sink.query.parallel=8
+# row or txn or embed
+sink.monitor.freshness.level=embed
+sink.monitor.freshness.embed.warmup=10
+sink.monitor.freshness.embed.static=false
+sink.monitor.freshness.embed.snapshot=true
+sink.monitor.freshness.embed.tablelist=binlog_test
+sink.monitor.freshness.verbose=true
+sink.monitor.freshness.timestamp=true
+sink.storage.loop=true
+# Kafka Config
+bootstrap.servers=realtime-kafka-2:29092
+group.id=3078
+auto.offset.reset=earliest
+key.deserializer=org.apache.kafka.common.serialization.StringDeserializer
+sink.kafka.value.format=json
+# Topic & Database Config
+topic.prefix=mysql-cdc
+consumer.capture_database=cdc_verify
+consumer.include_tables=
+sink.csv.path=./data
+sink.csv.enable_header=false
+## Retina Config
+sink.retina.embedded=false
+# stub or stream
+sink.retina.mode=stream
+sink.remote.host=localhost
+sink.remote.port=29422
+sink.timeout.ms=5000
+sink.flush.interval.ms=50
+sink.flush.batch.size=10
+sink.max.retries=3
+## writer commit
+# sync or async
+sink.commit.method=sync
+sink.commit.batch.size=10
+sink.commit.batch.worker=32
+sink.commit.batch.delay=3000
+## Proto Config
+sink.proto.dir=file:///home/ubuntu/disk1/hybench/
+sink.proto.data=hybench10_10
+sink.proto.maxRecords=100000
+## Flink Config
+sink.flink.server.port=9091
+## Schema Registry
+sink.registry.url=http://localhost:8080/apis/registry/v2
+# Transaction Config
+transaction.topic.suffix=transaction
+sink.trans.batch.size=100
+
+# Sink Metrics
+sink.monitor.enable=true
+sink.monitor.port=9464
+sink.monitor.report.interval=10000
+sink.monitor.freshness.interval=1000
+
+# Interact with other rpc
+sink.rpc.enable=true
+# debezium engine config
+debezium.name=mysql-cdc-engine
+debezium.connector.class=io.debezium.connector.mysql.MySqlConnector
+debezium.provide.transaction.metadata=true
+debezium.offset.storage=org.apache.kafka.connect.storage.FileOffsetBackingStore
+debezium.offset.storage.file.filename=/tmp/pixels-sink-mysql-offsets.dat
+debezium.offset.flush.interval.ms=60000
+debezium.schema.history.internal=io.debezium.storage.file.history.FileSchemaHistory
+debezium.schema.history.internal.file.filename=/tmp/pixels-sink-mysql-schema-history.dat
+debezium.database.hostname=tdsql
+debezium.database.port=6060
+debezium.database.user=tdsql
+debezium.database.password=tdsql123
+debezium.database.server.id=5401
+debezium.database.include.list=cdc_verify
+debezium.snapshot.mode=never
+debezium.key.converter=org.apache.kafka.connect.json.JsonConverter
+debezium.value.converter=org.apache.kafka.connect.json.JsonConverter
+debezium.topic.prefix=mysql-cdc
+debezium.transforms=topicRouting
+debezium.transforms.topicRouting.type=org.apache.kafka.connect.transforms.RegexRouter
+debezium.transforms.topicRouting.regex=mysql-cdc\\.cdc_verify\\.(.*)
+debezium.transforms.topicRouting.replacement=mysql-cdc.cdc_verify.$1
diff --git a/conf/pixels-sink.pg.properties b/conf/pixels-sink.pg.properties
index effc171..741aa2c 100644
--- a/conf/pixels-sink.pg.properties
+++ b/conf/pixels-sink.pg.properties
@@ -1,5 +1,9 @@
# engine | kafka | storage
sink.datasource=engine
+sink.datasource.engine.format=connect
+# CDC dialect for envelope normalization: mysql | postgresql
+sink.debezium.dialect=postgresql
+sink.storage.mode=stream
# -1 means no limit, Only implement in retina sink mode yet
sink.datasource.rate.limit=100000
# Sink Config: retina | csv | proto | flink | none
@@ -12,11 +16,11 @@ sink.monitor.report.enable=true
sink.monitor.report.file=/home/ubuntu/pixels-sink/resulti7i/100k_rate_2.csv
sink.monitor.freshness.file=/home/ubuntu/pixels-sink/resulti7i/100k_freshness_2.csv
# trino for freshness query
-trino.url=jdbc:trino://realtime-kafka-2:8080/pixels/pixels_bench_sf10x
-# trino.url=jdbc:trino://realtime-pixels-coordinator:8080/pixels/pixels_bench_sf10x
-trino.user=pixels
-trino.password=password
-trino.parallel=8
+sink.query.url=jdbc:trino://realtime-kafka-2:8080/pixels/pixels_bench_sf10x
+# sink.query.url=jdbc:trino://realtime-pixels-coordinator:8080/pixels/pixels_bench_sf10x
+sink.query.user=pixels
+sink.query.password=password
+sink.query.parallel=8
# row or txn or embed
sink.monitor.freshness.level=embed
sink.monitor.freshness.embed.warmup=10
@@ -31,8 +35,7 @@ bootstrap.servers=realtime-kafka-2:29092
group.id=3078
auto.offset.reset=earliest
key.deserializer=org.apache.kafka.common.serialization.StringDeserializer
-#value.deserializer=io.pixelsdb.pixels.writer.deserializer.RowChangeEventAvroDeserializer
-value.deserializer=io.pixelsdb.pixels.sink.event.deserializer.RowChangeEventJsonDeserializer
+sink.kafka.value.format=json
# Topic & Database Config
topic.prefix=postgresql.oltp_server
consumer.capture_database=pixels_bench_sf1x
@@ -66,8 +69,6 @@ sink.flink.server.port=9091
sink.registry.url=http://localhost:8080/apis/registry/v2
# Transaction Config
transaction.topic.suffix=transaction
-#transaction.topic.value.deserializer=io.pixelsdb.pixels.writer.deserializer.TransactionAvroMessageDeserializer
-transaction.topic.value.deserializer=io.pixelsdb.pixels.sink.event.deserializer.TransactionJsonMessageDeserializer
sink.trans.batch.size=100
# Sink Metrics
@@ -78,7 +79,6 @@ sink.monitor.freshness.interval=1000
# Interact with other rpc
sink.rpc.enable=true
-sink.rpc.mock.delay=20
# debezium engine config
debezium.name=testEngine
debezium.connector.class=io.debezium.connector.postgresql.PostgresConnector
diff --git a/docs/configuration.md b/docs/configuration.md
index 845af65..79cd195 100644
--- a/docs/configuration.md
+++ b/docs/configuration.md
@@ -12,14 +12,18 @@ Values are loaded by `PixelsSinkConfig` and mapped from keys in the properties f
| --- | --- | --- |
| `sink.datasource` | `engine` | Source type: `engine`, `kafka`, or `storage`. |
| `sink.mode` | `retina` | Sink type: `retina`, `csv`, `proto`, `flink`, or `none`. |
+| `sink.datasource.decode.threads` | `4` | Decode thread pool size. Engine uses `StreamOrderedDecoder` (multi logical stream); Storage uses `OrderedBatchDecoder` (single-key batch). |
+| `sink.datasource.engine.format` | `connect` | Engine wire format. Only `connect` is allowed; other values fail fast on resolve. |
| `sink.datasource.rate.limit` | `-1` | Rate limit for source ingestion. `-1` disables. |
| `sink.datasource.rate.limit.type` | `semaphore` | Rate limiter type used by `FlushRateLimiterFactory`. 'guava' or 'semaphore'|
### Notes on `sink.datasource`
-- `engine` reads CDC logs directly from Debezium Engine.
+- `engine` reads CDC logs directly from Debezium Engine. Currently requires `sink.datasource.engine.format=connect`. Json/Avro Engine paths are reserved and will reuse the same `conversion.debezium` converter interfaces later.
- `storage` reads CDC logs from files dumped by `sink.proto` output; schema reference: [sink.proto](https://github.com/pixelsdb/pixels/blob/master/proto/sink.proto).
- `kafka` reads from a set of Kafka topics; this mode is deprecated and not actively tested.
+- Engine and Kafka records are normalized to the canonical `SinkProto` contract before reaching writers.
+- Storage row records use canonical `SinkProto`; `SourceInfo.schema` may be empty for MySQL.
### Notes on `sink.mode`
@@ -50,10 +54,16 @@ Notes on `sink.trans.mode`:
| Key | Default | Notes |
| --- | --- | --- |
+| `sink.datasource.engine.format` | `connect` | Only `connect` is runnable today. Non-`connect` values fail fast. |
+| `sink.debezium.dialect` | none | Sink-side CDC dialect for envelope normalization: `mysql` or `postgresql`. Used by Engine and Kafka conversion. If unset, falls back to inferring from `debezium.connector.class`. |
| `debezium.name` | none | Engine name. |
-| `debezium.connector.class` | none | Connector class, e.g. PostgreSQL connector. |
+| `debezium.connector.class` | none | Debezium Engine connector class only (which DB the engine connects to). Not a Kafka setting. |
| `debezium.*` | none | Standard Debezium engine properties. |
+See `conf/pixels-sink.mysql.properties` for a TDSQL MySQL CDC example.
+
+The Debezium Connector reads the database Binlog or WAL. The local `conversion.debezium` package only converts envelopes already emitted by Debezium (`connect` / `json` / `avro` plus shared `support` / `dialect`).
+
### Retina Sink
| Key | Default | Notes |
@@ -89,6 +99,7 @@ Notes on `sink.trans.mode`:
| `sink.proto.dir` | required | Proto output or input directory. |
| `sink.proto.data` | `data` | Data set name. |
| `sink.proto.maxRecords` | `100000` | Max records per file. |
+| `sink.storage.mode` | `stream` | Storage read mode: `stream` reads records incrementally; `memory` preloads all records before replay. |
| `sink.storage.loop` | `false` | Whether to loop over stored files. |
### Flink Sink
@@ -108,15 +119,27 @@ Kafka source is deprecated.
| `group.id` | required | Consumer group id. |
| `auto.offset.reset` | none | Standard Kafka consumer property. |
| `key.deserializer` | `org.apache.kafka.common.serialization.StringDeserializer` | Kafka key deserializer. |
-| `value.deserializer` | `io.pixelsdb.pixels.sink.event.deserializer.RowChangeEventJsonDeserializer` | Kafka value deserializer for row events. |
+| `sink.kafka.value.format` | `json` | Envelope format: `json` or `avro`. Kafka sources assemble `conversion.debezium` converters from this key. |
+| `sink.debezium.dialect` | none | Upstream CDC dialect (`mysql` / `postgresql`). Required for Kafka transaction decoding; row events can also infer from `source.connector` when unset. |
| `topic.prefix` | required | Topic prefix for table events. |
| `consumer.capture_database` | required | Database name used to build topic names. |
| `consumer.include_tables` | empty | Comma-separated table list, empty means all. |
| `transaction.topic.suffix` | `transaction` | Suffix appended to transaction topics. |
-| `transaction.topic.value.deserializer` | `io.pixelsdb.pixels.sink.event.deserializer.RowChangeEventJsonDeserializer` | Deserializer for transaction messages. |
| `transaction.topic.group_id` | `transaction_consumer` | Consumer group for transaction topic. |
| `sink.registry.url` | required | Avro Schema registry endpoint. |
+**Legacy Kafka deserializer migration**
+
+Prefer `sink.kafka.value.format`. Old keys `value.deserializer` and
+`transaction.topic.value.deserializer` are no longer SPI; `PixelsSinkConfig`
+migrates them at startup:
+
+| Condition | Behavior |
+| --- | --- |
+| `sink.kafka.value.format` is set; legacy keys still present | Warn and ignore legacy keys |
+| Format unset; legacy class name maps to Json/Avro | Infer format and warn |
+| Row/tx inferences conflict, or class name unrecognized | Fail fast |
+
**Reserved Configuration**
| Key | Default | Notes |
@@ -124,7 +147,6 @@ Kafka source is deprecated.
| `sink.remote.host` | `localhost` | Sink server host. |
| `sink.remote.port` | `9090` | Sink server port. |
| `sink.rpc.enable` | `false` | Enable RPC simulation (for development). |
-| `sink.rpc.mock.delay` | `0` | Artificial delay in ms. |
**Monitoring and Metrics**
@@ -146,13 +168,13 @@ Kafka source is deprecated.
| `sink.monitor.freshness.verbose` | `false` | Verbose freshness logging. |
| `sink.monitor.freshness.timestamp` | `false` | Include timestamps. |
-Note: In the Retina paper experiments, `sink.monitor.freshness.level=embed` is used to query freshness from Trino. This requires the last column of each table to be `freshness_ts`.
+Note: In the Retina paper experiments, `sink.monitor.freshness.level=embed` queries freshness through JDBC. This requires the last column of each table to be `freshness_ts`. Trino is available in the default build; HiveServer2 support for Hudi requires building with `-Phudi-hive`.
-**Freshness Trino Settings**
+**Freshness Query Settings**
| Key | Default | Notes |
| --- | --- | --- |
-| `trino.url` | required for Trino-based freshness | JDBC URL. |
-| `trino.user` | required for Trino-based freshness | Username. |
-| `trino.password` | required for Trino-based freshness | Password. |
-| `trino.parallel` | `1` | Parallel query count. |
+| `sink.query.url` | required for embedded freshness | Trino or HiveServer2 JDBC URL. |
+| `sink.query.user` | required for embedded freshness | Username. |
+| `sink.query.password` | empty | Password. |
+| `sink.query.parallel` | `1` | Parallel query count. |
diff --git a/docs/overview.md b/docs/overview.md
index c321e20..952cfa8 100644
--- a/docs/overview.md
+++ b/docs/overview.md
@@ -15,86 +15,83 @@ Pixels Sink uses a multi-stage pipeline. Each stage communicates via producer/co
- Receives a `ConfigFactory` directly and builds the same sink pipeline.
**Source**
-The source stage pulls events and forwards raw payloads to providers.
+The source stage owns transport lifecycle (Engine / Kafka / Storage). Payload
+conversion is separate from transport:
+
+- Parallel decode infrastructure lives in `util.concurrent`
+ (`DecodeExecutors`, `StreamOrderedDecoder`, `OrderedBatchDecoder`) and is
+ sized by `sink.datasource.decode.threads`.
+- Debezium envelopes (Engine Connect / Kafka JSON / Kafka Avro) convert through
+ `conversion.debezium` (`connect` / `json` / `avro` + `support` / `dialect`).
+- Storage sink-proto bytes convert through `conversion.sinkproto` and stay
+ independent of the Debezium package.
+- Engine event classification stays in `source.engine` (`ConnectEventClassifier`,
+ `DebeziumRecordType`).
+
+**Decode ordering (not enhanced beyond historical contract)**
+
+| Guaranteed | Not guaranteed |
+| --- | --- |
+| Same table (Engine) / same Storage key: FIFO relative to source scan or enqueue order | Cross-table global order |
+| TX events FIFO on the TX stream | TX BEGIN/END vs row events global arrival order |
+| | Same-table cross-PK-bucket write total order; `totalOrder` / `dataCollectionOrder` reorder |
+
+Two decoders have different roles (not a shared abstraction):
+
+| Component | Role | Stream / shard key |
+| --- | --- | --- |
+| `StreamOrderedDecoder` | Engine: ordered publish across multiple logical streams in a mixed batch | ROW = `SchemaTableName`; TX = singleton stream key |
+| `OrderedBatchDecoder` | Storage: parallel decode inside one key consumer, then deliver in input order | Sharding already done by `queueMap`; decoder does not re-shard |
**Source Inputs**
| Source Type | Description | Related Config |
| --- | --- | --- |
-| `engine` | Debezium Engine reads WAL/binlog directly from a database | `debezium.*` |
-| `kafka` | Kafka consumer reads change events from topics | `bootstrap.servers`, `group.id`, `topic.*` |
+| `engine` | Debezium Engine reads WAL/binlog directly from a database | `debezium.*`, `sink.datasource.engine.format` |
+| `kafka` | Kafka consumer reads change events from topics | `bootstrap.servers`, `group.id`, `topic.*`, `sink.kafka.value.format` |
| `storage` | Reads from Pixels storage files containing serialized sink proto records | `sink.proto.*`, `sink.storage.loop` |
**Source Outputs**
-- The source does not parse events. It forwards raw records to providers.
+- Kafka, Engine, and Storage sources publish only canonical
+ `RowChangeEvent` or `SinkProto.TransactionMetadata` objects.
+- Protocol conversion is implemented in `conversion`; pipeline queues never receive
+ Kafka bytes, Engine `SourceRecord`, or Storage `ByteBuffer`.
-**Provider**
-Providers convert source records into Pixels events.
+**Pipeline Queue**
+Pipelines own bounded canonical-event queues. The queue provides backpressure,
+ordered delivery, and lifecycle management without knowing the source protocol.
```mermaid
classDiagram
direction TB
- class EventProvider~SOURCE_RECORD_T, TARGET_RECORD_T~ {
- +run()
+ class BlockingBoundedQueue~T~ {
+ +put(T value)
+ +take()
+close()
- +processLoop()
- +convertToTargetRecord()
- +recordSerdEvent()
- +putRawEvent()
- +getRawEvent()
- +pollRawEvent()
- +putTargetEvent()
- +getTargetEvent()
}
- class TableEventProvider~SOURCE_RECORD_T~ {
+ class TablePipeline {
+ +publish(RowChangeEvent)
}
- class TableEventEngineProvider~T~ {
+ class TransactionPipeline {
+ +publish(TransactionMetadata)
}
- class TableEventKafkaProvider~T~ {
- }
- class TableEventStorageProvider~T~ {
+ class TablePipelineManager {
+ +route(RowChangeEvent)
}
- class TransactionEventProvider~SOURCE_RECORD_T~ {
- }
- class TransactionEventEngineProvider~T~ {
- }
- class TransactionEventKafkaProvider~T~ {
- }
- class TransactionEventStorageProvider~T~ {
- }
-
- EventProvider <|-- TableEventProvider
- EventProvider <|-- TransactionEventProvider
-
- TableEventProvider <|-- TableEventEngineProvider
- TableEventProvider <|-- TableEventKafkaProvider
- TableEventProvider <|-- TableEventStorageProvider
-
- TransactionEventProvider <|-- TransactionEventEngineProvider
- TransactionEventProvider <|-- TransactionEventKafkaProvider
- TransactionEventProvider <|-- TransactionEventStorageProvider
+ TablePipelineManager --> TablePipeline
+ TablePipeline --> BlockingBoundedQueue
+ TransactionPipeline --> BlockingBoundedQueue
```
-Example mappings:
-
-| Provider | Source Type | Target Type |
-| --- | --- | --- |
-| `TableEventEngineProvider` | Debezium Struct | `RowChangeEvent` |
-| `TableEventKafkaProvider` | Kafka topic | `RowChangeEvent` |
-| `TableEventStorageProvider` | Proto bytes | `RowChangeEvent` |
-| `TransactionEventEngineProvider` | Debezium Struct | `SinkProto.TransactionMetadata` |
-| `TransactionEventKafkaProvider` | Kafka topic | `SinkProto.TransactionMetadata` |
-| `TransactionEventStorageProvider` | Proto bytes | `SinkProto.TransactionMetadata` |
-
**Processor**
-Processors pull events from providers and write to the sink writers.
+Processors pull events from pipeline queues and write to the sink writers.
-- `TableProcessor` instances are created by `TableProviderAndProcessorPipelineManager`.
+- `TableProcessor` instances are created by `TablePipelineManager`.
- There is typically one `TableProcessor` per table to maintain per-table ordering.
-- `TransactionProcessor` is a singleton.
+- `TransactionPipeline` owns the transaction queue and `TransactionProcessor`.
**Writer**
Writers implement `PixelsSinkWriter`:
diff --git a/docs/usage.md b/docs/usage.md
index 5beaa54..d27ead6 100644
--- a/docs/usage.md
+++ b/docs/usage.md
@@ -25,7 +25,8 @@ mvn -q -DskipTests exec:java \
**Pipeline Summary**
- A source reads change events from Debezium Engine, Kafka, or storage files.
-- A provider converts source records into Pixels events.
+- Conversion normalizes source records into canonical Pixels events.
+- Pipelines route events through bounded queues to their processors.
- Processors enforce ordering by table and write to the configured sink.
- The sink writer persists the events to Retina, CSV, Proto, Flink, or a no-op sink.
diff --git a/pom.xml b/pom.xml
index 2b965f2..f9203eb 100644
--- a/pom.xml
+++ b/pom.xml
@@ -34,14 +34,40 @@
true0.9.0
- 3.8.0
- 5.8
+ 3.9.11.18.423.2.3.Final
+ 2.16.2
+ 2.6.2.Final
+ 1.11.3
+ 3.6.1
+ 3.12.0
+
+ 440
+ 2.3.91.4.13
- 440
+
+
+
+
+ com.fasterxml.jackson
+ jackson-bom
+ ${dep.jackson.version}
+ pom
+ import
+
+
+ io.netty
+ netty-bom
+ ${dep.netty.version}
+ pom
+ import
+
+
+
+
io.pixelsdb
@@ -54,56 +80,82 @@
true
- io.pixelsdb
- pixels-retina
+ io.etcd
+ jetcd-coretrue
- test
+
- org.apache.hive
- hive-jdbc
- 2.3.9
+ io.netty
+ netty-codec-http2
+ runtime
- io.etcd
- jetcd-core
- true
+ io.netty
+ netty-handler-proxy
+ runtimeio.netty
- netty-all
+ netty-resolver-dns
+ runtime
- io.grpc
- grpc-netty
+ io.netty
+ netty-transport-native-unix-common
+ runtimeio.trinotrino-jdbc
- ${trino.version}
+ runtime
- com.alibaba
- fastjson
- true
+ org.apache.logging.log4j
+ log4j-api
+ runtimeorg.apache.logging.log4jlog4j-coretrue
+ runtimeorg.apache.logging.log4jlog4j-slf4j-impl
+ runtime
+
+
+
+ org.slf4j
+ log4j-over-slf4j
+ runtime
+
+
+
+ org.apache.logging.log4j
+ log4j-jul${dep.log4j.version}
+ runtime
-
+
io.grpcgrpc-netty-shadedtrue
+ runtime
+
+
+
+ io.grpc
+ grpc-netty
+ runtimeio.grpc
@@ -116,14 +168,12 @@
true
- com.google.protobuf
- protobuf-java-util
- 3.25.1
+ io.grpc
+ grpc-api
-
- javax.annotation
- javax.annotation-api
+ com.google.protobuf
+ protobuf-java
@@ -144,16 +194,14 @@
connect-api${dep.kafka.version}
-
- com.opencsv
- opencsv
- ${dep.opencsv.version}
+ org.slf4j
+ slf4j-api
+
com.google.guavaguava
- 33.2.0-jre
@@ -163,6 +211,19 @@
provided
+
+ com.fasterxml.jackson.core
+ jackson-core
+
+
+ com.fasterxml.jackson.core
+ jackson-databind
+
+
+ org.apache.avro
+ avro
+ ${dep.avro.version}
+
@@ -174,84 +235,81 @@
io.debeziumdebezium-embedded${dep.debezium.version}
+
+
+
+ ch.qos.reload4j
+ reload4j
+
+ io.debezium
- debezium-sink
+ debezium-connector-postgres${dep.debezium.version}
+ runtimeio.debezium
- debezium-connector-postgres
+ debezium-connector-mysql${dep.debezium.version}
+ runtime
- junit
- junit
- test
- true
-
-
- org.junit.platform
- junit-platform-launcher
- test
- true
-
-
- org.junit.platform
- junit-platform-runner
+ org.junit.jupiter
+ junit-jupiter
+ ${dep.junit.jupiter.version}test
- true
- org.junit.jupiter
- junit-jupiter
- 5.8.2
- test
+ io.apicurio
+ apicurio-registry-serdes-avro-serde
+ ${dep.apicurio.version}
- org.junit.jupiter
- junit-jupiter-api
- 5.8.2
- test
+ io.apicurio
+ apicurio-registry-serde-common
+ ${dep.apicurio.version}
-
io.apicurio
- apicurio-registry-serdes-avro-serde
- 2.6.2.Final
+ apicurio-registry-client
+ ${dep.apicurio.version}io.prometheussimpleclient
- 0.16.0io.prometheussimpleclient_hotspot
- 0.16.0
-
io.prometheussimpleclient_httpserver
- 0.16.0
+ ${dep.prometheus.client.version}org.apache.commonscommons-math3
- 3.6.1
+ ${dep.commons-math3.version}
+
+
+ org.apache.commons
+ commons-lang3
+ ${dep.commons-lang3.version}
-
+
io.pixelsdbpixels-storage-localfs
+ runtime
@@ -265,10 +323,21 @@
+
+ org.apache.maven.plugins
+ maven-surefire-plugin
+
+ false
+ integration
+ --add-opens=java.base/java.nio=ALL-UNNAMED
+ --add-exports=java.base/sun.nio.ch=ALL-UNNAMED
+ -Djava.util.logging.manager=org.apache.logging.log4j.jul.LogManager
+
+ org.apache.maven.pluginsmaven-shade-plugin
- 3.2.0
+ ${maven.plugin.shade.version}package
@@ -278,6 +347,7 @@
pixels-sink${project.basedir}/target
+ falsetruefull
@@ -287,6 +357,35 @@
META-INF/*.SFMETA-INF/*.DSAMETA-INF/*.RSA
+ module-info.class
+ META-INF/versions/**/module-info.class
+
+
+
+ io.grpc:grpc-netty
+
+ META-INF/services/io.grpc.ServerProvider
+ META-INF/services/io.grpc.ManagedChannelProvider
+
+
+
+
+ org.apache.hive:hive-jdbc
+
+ org/apache/logging/log4j/**
+ org/apache/logging/slf4j/**
+ com/fasterxml/jackson/**
+ javax/servlet/**
+ org/openjdk/jol/**
+ META-INF/org/apache/logging/log4j/**
+ META-INF/services/org.apache.logging.log4j.*
+ META-INF/services/com.fasterxml.jackson.*
+ META-INF/services/javax.servlet.ServletContainerInitializer
+ META-INF/maven/org.apache.logging.log4j/**
+ META-INF/maven/com.fasterxml.jackson.core/**
+ META-INF/maven/javax.servlet/**
+ META-INF/maven/org.openjdk.jol/**
@@ -296,6 +395,11 @@
${mainClass}
+
+
+ true
+
@@ -335,4 +439,45 @@
+
+
+
+
+ hudi-hive
+
+
+ org.apache.hive
+ hive-jdbc
+ ${dep.hive.jdbc.version}
+ standalone
+ runtime
+
+
+ *
+ *
+
+
+
+
+
+
+ integration-tests
+
+
+
+ org.apache.maven.plugins
+ maven-surefire-plugin
+
+ false
+
+ --add-opens=java.base/java.nio=ALL-UNNAMED
+ --add-exports=java.base/sun.nio.ch=ALL-UNNAMED
+ -Djava.util.logging.manager=org.apache.logging.log4j.jul.LogManager
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/main/java/io/pixelsdb/pixels/sink/PixelsSinkApp.java b/src/main/java/io/pixelsdb/pixels/sink/PixelsSinkApp.java
index 0453d6a..a55afb4 100644
--- a/src/main/java/io/pixelsdb/pixels/sink/PixelsSinkApp.java
+++ b/src/main/java/io/pixelsdb/pixels/sink/PixelsSinkApp.java
@@ -1,23 +1,18 @@
/*
* Copyright 2025 PixelsDB.
*
- * This file is part of Pixels.
+ * Licensed 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
*
- * Pixels is free software: you can redistribute it and/or modify
- * it under the terms of the Affero GNU General Public License as
- * published by the Free Software Foundation, either version 3 of
- * the License, or (at your option) any later version.
+ * http://www.apache.org/licenses/LICENSE-2.0
*
- * Pixels is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * Affero GNU General Public License for more details.
- *
- * You should have received a copy of the Affero GNU General Public
- * License along with Pixels. If not, see
- * .
+ * 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 io.pixelsdb.pixels.sink;
import io.pixelsdb.pixels.sink.config.CommandLineConfig;
@@ -53,8 +48,8 @@ public static void main(String[] args) throws IOException
Runtime.getRuntime().addShutdownHook(new Thread(() ->
{
PixelsSinkConfig config = PixelsSinkConfigFactory.getInstance();
+ sinkSource.close();
TransactionProxy.staticClose();
- sinkSource.stopProcessor();
LOGGER.info("Pixels Sink Server shutdown complete");
if (config.getSinkMonitorFreshnessLevel().equals("embed") && freshnessClient != null)
{
diff --git a/src/main/java/io/pixelsdb/pixels/sink/PixelsSinkProvider.java b/src/main/java/io/pixelsdb/pixels/sink/PixelsSinkProvider.java
index ecd9364..2f808db 100644
--- a/src/main/java/io/pixelsdb/pixels/sink/PixelsSinkProvider.java
+++ b/src/main/java/io/pixelsdb/pixels/sink/PixelsSinkProvider.java
@@ -1,23 +1,18 @@
/*
* Copyright 2025 PixelsDB.
*
- * This file is part of Pixels.
+ * Licensed 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
*
- * Pixels is free software: you can redistribute it and/or modify
- * it under the terms of the Affero GNU General Public License as
- * published by the Free Software Foundation, either version 3 of
- * the License, or (at your option) any later version.
+ * http://www.apache.org/licenses/LICENSE-2.0
*
- * Pixels is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * Affero GNU General Public License for more details.
- *
- * You should have received a copy of the Affero GNU General Public
- * License along with Pixels. If not, see
- * .
+ * 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 io.pixelsdb.pixels.sink;
import io.pixelsdb.pixels.common.sink.SinkProvider;
@@ -42,7 +37,7 @@ public void start(ConfigFactory config)
@Override
public void shutdown()
{
- sinkSource.stopProcessor();
+ sinkSource.close();
}
@Override
diff --git a/src/main/java/io/pixelsdb/pixels/sink/config/CommandLineConfig.java b/src/main/java/io/pixelsdb/pixels/sink/config/CommandLineConfig.java
index a8bf062..ab87dd6 100644
--- a/src/main/java/io/pixelsdb/pixels/sink/config/CommandLineConfig.java
+++ b/src/main/java/io/pixelsdb/pixels/sink/config/CommandLineConfig.java
@@ -1,23 +1,18 @@
/*
* Copyright 2025 PixelsDB.
*
- * This file is part of Pixels.
+ * Licensed 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
*
- * Pixels is free software: you can redistribute it and/or modify
- * it under the terms of the Affero GNU General Public License as
- * published by the Free Software Foundation, either version 3 of
- * the License, or (at your option) any later version.
+ * http://www.apache.org/licenses/LICENSE-2.0
*
- * Pixels is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * Affero GNU General Public License for more details.
- *
- * You should have received a copy of the Affero GNU General Public
- * License along with Pixels. If not, see
- * .
+ * 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 io.pixelsdb.pixels.sink.config;
diff --git a/src/main/java/io/pixelsdb/pixels/sink/config/ConfigKey.java b/src/main/java/io/pixelsdb/pixels/sink/config/ConfigKey.java
index bb4ed66..461ff26 100644
--- a/src/main/java/io/pixelsdb/pixels/sink/config/ConfigKey.java
+++ b/src/main/java/io/pixelsdb/pixels/sink/config/ConfigKey.java
@@ -1,23 +1,18 @@
/*
- * Copyright 2025 PixelsDB.
+ * Copyright 2023 PixelsDB.
*
- * This file is part of Pixels.
+ * Licensed 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
*
- * Pixels is free software: you can redistribute it and/or modify
- * it under the terms of the Affero GNU General Public License as
- * published by the Free Software Foundation, either version 3 of
- * the License, or (at your option) any later version.
+ * http://www.apache.org/licenses/LICENSE-2.0
*
- * Pixels is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * Affero GNU General Public License for more details.
- *
- * You should have received a copy of the Affero GNU General Public
- * License along with Pixels. If not, see
- * .
+ * 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 io.pixelsdb.pixels.sink.config;
diff --git a/src/main/java/io/pixelsdb/pixels/sink/config/ConfigLoader.java b/src/main/java/io/pixelsdb/pixels/sink/config/ConfigLoader.java
index 0461535..d5f1a66 100644
--- a/src/main/java/io/pixelsdb/pixels/sink/config/ConfigLoader.java
+++ b/src/main/java/io/pixelsdb/pixels/sink/config/ConfigLoader.java
@@ -1,23 +1,18 @@
/*
* Copyright 2025 PixelsDB.
*
- * This file is part of Pixels.
+ * Licensed 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
*
- * Pixels is free software: you can redistribute it and/or modify
- * it under the terms of the Affero GNU General Public License as
- * published by the Free Software Foundation, either version 3 of
- * the License, or (at your option) any later version.
+ * http://www.apache.org/licenses/LICENSE-2.0
*
- * Pixels is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * Affero GNU General Public License for more details.
- *
- * You should have received a copy of the Affero GNU General Public
- * License along with Pixels. If not, see
- * .
+ * 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 io.pixelsdb.pixels.sink.config;
diff --git a/src/main/java/io/pixelsdb/pixels/sink/config/EngineValueFormat.java b/src/main/java/io/pixelsdb/pixels/sink/config/EngineValueFormat.java
new file mode 100644
index 0000000..e1d0303
--- /dev/null
+++ b/src/main/java/io/pixelsdb/pixels/sink/config/EngineValueFormat.java
@@ -0,0 +1,52 @@
+/*
+ * Copyright 2026 PixelsDB.
+ *
+ * Licensed 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 io.pixelsdb.pixels.sink.config;
+
+import java.util.Locale;
+
+/**
+ * Resolves {@code sink.datasource.engine.format}. Only {@code connect} is implemented.
+ */
+public final class EngineValueFormat
+{
+ public static final String CONNECT = "connect";
+
+ private EngineValueFormat()
+ {
+ }
+
+ public static String resolve(String configuredFormat)
+ {
+ if (configuredFormat == null || configuredFormat.isBlank())
+ {
+ return CONNECT;
+ }
+ String normalized = normalize(configuredFormat);
+ if (!CONNECT.equals(normalized))
+ {
+ throw new IllegalArgumentException(
+ "sink.datasource.engine.format='" + configuredFormat +
+ "' is not implemented yet; only 'connect' is supported. " +
+ "Json/Avro Engine paths will reuse conversion.debezium converters later.");
+ }
+ return normalized;
+ }
+
+ private static String normalize(String format)
+ {
+ return format.trim().toLowerCase(Locale.ROOT);
+ }
+}
diff --git a/src/main/java/io/pixelsdb/pixels/sink/config/KafkaValueFormat.java b/src/main/java/io/pixelsdb/pixels/sink/config/KafkaValueFormat.java
new file mode 100644
index 0000000..0a0721e
--- /dev/null
+++ b/src/main/java/io/pixelsdb/pixels/sink/config/KafkaValueFormat.java
@@ -0,0 +1,51 @@
+/*
+ * Copyright 2026 PixelsDB.
+ *
+ * Licensed 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 io.pixelsdb.pixels.sink.config;
+
+import java.util.Locale;
+
+/**
+ * Resolves {@code sink.kafka.value.format}. Supported values: {@code json}, {@code avro}.
+ */
+public final class KafkaValueFormat
+{
+ public static final String JSON = "json";
+ public static final String AVRO = "avro";
+
+ private KafkaValueFormat()
+ {
+ }
+
+ public static String resolve(String configuredFormat)
+ {
+ if (configuredFormat == null || configuredFormat.isBlank())
+ {
+ return JSON;
+ }
+ return normalize(configuredFormat);
+ }
+
+ private static String normalize(String format)
+ {
+ String normalized = format.trim().toLowerCase(Locale.ROOT);
+ if (!JSON.equals(normalized) && !AVRO.equals(normalized))
+ {
+ throw new IllegalArgumentException(
+ "sink.kafka.value.format must be json or avro, got: " + format);
+ }
+ return normalized;
+ }
+}
diff --git a/src/main/java/io/pixelsdb/pixels/sink/config/PixelsSinkConfig.java b/src/main/java/io/pixelsdb/pixels/sink/config/PixelsSinkConfig.java
index 4f75eeb..02108b7 100644
--- a/src/main/java/io/pixelsdb/pixels/sink/config/PixelsSinkConfig.java
+++ b/src/main/java/io/pixelsdb/pixels/sink/config/PixelsSinkConfig.java
@@ -1,39 +1,43 @@
/*
* Copyright 2025 PixelsDB.
*
- * This file is part of Pixels.
+ * Licensed 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
*
- * Pixels is free software: you can redistribute it and/or modify
- * it under the terms of the Affero GNU General Public License as
- * published by the Free Software Foundation, either version 3 of
- * the License, or (at your option) any later version.
+ * http://www.apache.org/licenses/LICENSE-2.0
*
- * Pixels is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * Affero GNU General Public License for more details.
- *
- * You should have received a copy of the Affero GNU General Public
- * License along with Pixels. If not, see
- * .
+ * 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 io.pixelsdb.pixels.sink.config;
import io.pixelsdb.pixels.common.utils.ConfigFactory;
-import io.pixelsdb.pixels.sink.event.deserializer.RowChangeEventJsonDeserializer;
import io.pixelsdb.pixels.sink.writer.PixelsSinkMode;
import io.pixelsdb.pixels.sink.writer.retina.RetinaServiceProxy;
import io.pixelsdb.pixels.sink.writer.retina.TransactionMode;
import lombok.Getter;
import org.apache.kafka.common.serialization.StringDeserializer;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.util.List;
+import java.util.Locale;
+import java.util.Properties;
@Getter
public class PixelsSinkConfig
{
+ private static final Logger LOGGER = LoggerFactory.getLogger(PixelsSinkConfig.class);
+ private static final String LEGACY_VALUE_DESERIALIZER = "value.deserializer";
+ private static final String LEGACY_TX_VALUE_DESERIALIZER =
+ "transaction.topic.value.deserializer";
+ private static final String KAFKA_VALUE_FORMAT_KEY = "sink.kafka.value.format";
+
private final ConfigFactory config;
@ConfigKey(value = "transaction.timeout", defaultValue = TransactionConfig.DEFAULT_TRANSACTION_TIME_OUT)
@@ -117,9 +121,6 @@ public class PixelsSinkConfig
@ConfigKey(value = "sink.rpc.enable", defaultValue = "false")
private boolean rpcEnable;
- @ConfigKey(value = "sink.rpc.mock.delay", defaultValue = "0")
- private int mockRpcDelay;
-
@ConfigKey(value = "sink.trans.batch.size", defaultValue = "100")
private int transBatchSize;
@@ -136,6 +137,16 @@ public class PixelsSinkConfig
@ConfigKey("debezium.topic.prefix")
private String debeziumTopicPrefix;
+ /**
+ * Sink-side CDC dialect for envelope normalization: {@code mysql} or {@code postgresql}.
+ * Independent of {@code debezium.connector.class}, which is only for Debezium Engine.
+ */
+ @ConfigKey(value = "sink.debezium.dialect", defaultValue = "")
+ private String debeziumDialect;
+
+ @ConfigKey(value = "debezium.connector.class", defaultValue = "")
+ private String debeziumConnectorClass;
+
@ConfigKey("consumer.capture_database")
private String captureDatabase;
@@ -151,8 +162,11 @@ public class PixelsSinkConfig
@ConfigKey(value = "key.deserializer", defaultClass = StringDeserializer.class)
private String keyDeserializer;
- @ConfigKey(value = "value.deserializer", defaultClass = RowChangeEventJsonDeserializer.class)
- private String valueDeserializer;
+ /**
+ * Kafka envelope wire format: {@code json} or {@code avro}.
+ */
+ @ConfigKey(value = "sink.kafka.value.format", defaultValue = KafkaValueFormat.JSON)
+ private String kafkaValueFormat;
@ConfigKey(value = "sink.csv.path", defaultValue = PixelsSinkDefaultConfig.CSV_SINK_PATH)
private String csvSinkPath;
@@ -160,10 +174,6 @@ public class PixelsSinkConfig
@ConfigKey(value = "transaction.topic.suffix", defaultValue = TransactionConfig.DEFAULT_TRANSACTION_TOPIC_SUFFIX)
private String transactionTopicSuffix;
- @ConfigKey(value = "transaction.topic.value.deserializer",
- defaultClass = RowChangeEventJsonDeserializer.class)
- private String transactionTopicValueDeserializer;
-
@ConfigKey(value = "transaction.topic.group_id",
defaultValue = TransactionConfig.DEFAULT_TRANSACTION_TOPIC_GROUP_ID)
private String transactionTopicGroupId;
@@ -177,6 +187,16 @@ public class PixelsSinkConfig
@ConfigKey(value = "sink.datasource", defaultValue = PixelsSinkDefaultConfig.DATA_SOURCE)
private String dataSource;
+ /**
+ * Engine wire format. Currently only {@code connect} is implemented.
+ */
+ @ConfigKey(value = "sink.datasource.engine.format", defaultValue = "connect")
+ private String engineFormat;
+
+ @ConfigKey(value = "sink.datasource.decode.threads",
+ defaultValue = PixelsSinkDefaultConfig.SOURCE_DECODE_THREADS)
+ private int sourceDecodeThreads;
+
@ConfigKey(value = "sink.datasource.rate.limit", defaultValue = "-1")
private int sourceRateLimit;
@@ -196,6 +216,9 @@ public class PixelsSinkConfig
@ConfigKey(value = "sink.storage.loop", defaultValue = "false")
private boolean sinkStorageLoop;
+ @ConfigKey(value = "sink.storage.mode", defaultValue = PixelsSinkDefaultConfig.STORAGE_MODE)
+ private String sinkStorageMode;
+
@ConfigKey(value = "sink.monitor.freshness.level", defaultValue = "row") // row or txn or embed
private String sinkMonitorFreshnessLevel;
@ConfigKey(value = "sink.monitor.freshness.embed.warmup", defaultValue = "10")
@@ -219,17 +242,17 @@ public class PixelsSinkConfig
@ConfigKey(value = "sink.monitor.freshness.timestamp", defaultValue = "false")
private boolean sinkMonitorFreshnessTimestamp;
- @ConfigKey(value = "trino.url")
- private String trinoUrl;
+ @ConfigKey(value = "sink.query.url")
+ private String sinkQueryUrl;
- @ConfigKey(value = "trino.user")
- private String trinoUser;
+ @ConfigKey(value = "sink.query.user")
+ private String sinkQueryUser;
- @ConfigKey(value = "trino.password")
- private String trinoPassword;
+ @ConfigKey(value = "sink.query.password")
+ private String sinkQueryPassword;
- @ConfigKey(value = "trino.parallel", defaultValue = "1")
- private int trinoParallel;
+ @ConfigKey(value = "sink.query.parallel", defaultValue = "1")
+ private int sinkQueryParallel;
public PixelsSinkConfig(String configFilePath) throws IOException
{
@@ -249,10 +272,124 @@ public String[] getIncludeTables()
return includeTablesRaw.isEmpty() ? new String[0] : includeTablesRaw.split(",");
}
- private void init()
+ /**
+ * Token used to select a {@code DebeziumSourceAdapter}.
+ * Prefers {@code sink.debezium.dialect}; falls back to {@code debezium.connector.class}
+ * so Engine-only configs keep working without duplicating the dialect.
+ */
+ public String resolveDebeziumSourceDialect()
{
- ConfigLoader.load(this.config.extractPropertiesByPrefix("", false), this);
+ if (debeziumDialect != null && !debeziumDialect.isBlank())
+ {
+ return debeziumDialect.trim();
+ }
+ if (debeziumConnectorClass != null && !debeziumConnectorClass.isBlank())
+ {
+ return debeziumConnectorClass.trim();
+ }
+ return "";
+ }
+ private void init()
+ {
+ Properties props = this.config.extractPropertiesByPrefix("", false);
+ ConfigLoader.load(props, this);
+
+ if (this.sourceDecodeThreads <= 0)
+ {
+ throw new IllegalArgumentException(
+ "sink.datasource.decode.threads must be positive");
+ }
this.enableSourceRateLimit = this.sourceRateLimit >= 0;
+ this.kafkaValueFormat = resolveKafkaValueFormat(props);
+ this.engineFormat = EngineValueFormat.resolve(this.engineFormat);
+ }
+
+ /**
+ * Migrates legacy {@code value.deserializer} /
+ * {@code transaction.topic.value.deserializer} class names to
+ * {@code sink.kafka.value.format}. Package-visible for unit tests.
+ */
+ static String resolveKafkaValueFormat(Properties props)
+ {
+ String explicitFormat = trimToNull(props.getProperty(KAFKA_VALUE_FORMAT_KEY));
+ String rowDeserializer = trimToNull(props.getProperty(LEGACY_VALUE_DESERIALIZER));
+ String txDeserializer = trimToNull(props.getProperty(LEGACY_TX_VALUE_DESERIALIZER));
+ boolean hasLegacy = rowDeserializer != null || txDeserializer != null;
+
+ if (explicitFormat != null)
+ {
+ if (hasLegacy)
+ {
+ LOGGER.warn(
+ "Ignoring legacy {} / {} because {} is set to '{}'",
+ LEGACY_VALUE_DESERIALIZER,
+ LEGACY_TX_VALUE_DESERIALIZER,
+ KAFKA_VALUE_FORMAT_KEY,
+ explicitFormat);
+ }
+ return KafkaValueFormat.resolve(explicitFormat);
+ }
+
+ if (!hasLegacy)
+ {
+ return KafkaValueFormat.resolve(null);
+ }
+
+ String inferredRow = inferFormatFromDeserializer(rowDeserializer);
+ String inferredTx = inferFormatFromDeserializer(txDeserializer);
+ if (inferredRow == null && inferredTx == null)
+ {
+ throw new IllegalArgumentException(
+ "Unable to migrate legacy Kafka deserializer classes to " +
+ KAFKA_VALUE_FORMAT_KEY + ": value.deserializer=" +
+ rowDeserializer + ", transaction.topic.value.deserializer=" +
+ txDeserializer);
+ }
+ if (inferredRow != null && inferredTx != null && !inferredRow.equals(inferredTx))
+ {
+ throw new IllegalArgumentException(
+ "Conflicting legacy Kafka deserializer formats: value.deserializer=" +
+ rowDeserializer + " (" + inferredRow +
+ "), transaction.topic.value.deserializer=" +
+ txDeserializer + " (" + inferredTx + ")");
+ }
+ String inferred = inferredRow != null ? inferredRow : inferredTx;
+ LOGGER.warn(
+ "Migrating legacy Kafka deserializer class names to {}={}",
+ KAFKA_VALUE_FORMAT_KEY, inferred);
+ return inferred;
+ }
+
+ private static String inferFormatFromDeserializer(String className)
+ {
+ if (className == null)
+ {
+ return null;
+ }
+ String simpleName = className;
+ int lastDot = className.lastIndexOf('.');
+ if (lastDot >= 0 && lastDot + 1 < className.length())
+ {
+ simpleName = className.substring(lastDot + 1);
+ }
+ String lower = simpleName.toLowerCase(Locale.ROOT);
+ boolean json = lower.contains("json");
+ boolean avro = lower.contains("avro");
+ if (json == avro)
+ {
+ return null;
+ }
+ return json ? KafkaValueFormat.JSON : KafkaValueFormat.AVRO;
+ }
+
+ private static String trimToNull(String value)
+ {
+ if (value == null)
+ {
+ return null;
+ }
+ String trimmed = value.trim();
+ return trimmed.isEmpty() ? null : trimmed;
}
}
\ No newline at end of file
diff --git a/src/main/java/io/pixelsdb/pixels/sink/config/PixelsSinkConstants.java b/src/main/java/io/pixelsdb/pixels/sink/config/PixelsSinkConstants.java
index 034ae78..4e5e1c5 100644
--- a/src/main/java/io/pixelsdb/pixels/sink/config/PixelsSinkConstants.java
+++ b/src/main/java/io/pixelsdb/pixels/sink/config/PixelsSinkConstants.java
@@ -1,30 +1,28 @@
/*
- * Copyright 2025 PixelsDB.
+ * Copyright 2023 PixelsDB.
*
- * This file is part of Pixels.
+ * Licensed 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
*
- * Pixels is free software: you can redistribute it and/or modify
- * it under the terms of the Affero GNU General Public License as
- * published by the Free Software Foundation, either version 3 of
- * the License, or (at your option) any later version.
+ * http://www.apache.org/licenses/LICENSE-2.0
*
- * Pixels is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * Affero GNU General Public License for more details.
- *
- * You should have received a copy of the Affero GNU General Public
- * License along with Pixels. If not, see
- * .
+ * 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 io.pixelsdb.pixels.sink.config;
public final class PixelsSinkConstants
{
public static final String ROW_RECORD_KAFKA_PROP_FACTORY = "row-record";
public static final String TRANSACTION_KAFKA_PROP_FACTORY = "transaction";
- public static final int MONITOR_NUM = 2;
+ public static final String KAFKA_VALUE_FORMAT = "pixels.sink.kafka.value.format";
+ /** Sink-side CDC dialect for envelope normalization: {@code mysql} or {@code postgresql}. */
+ public static final String SINK_DEBEZIUM_DIALECT = "sink.debezium.dialect";
+ public static final String DEBEZIUM_CONNECTOR_CLASS = "debezium.connector.class";
public static final int MAX_QUEUE_SIZE = 1_000;
public static final String SNAPSHOT_TX_PREFIX = "SNAPSHOT-";
diff --git a/src/main/java/io/pixelsdb/pixels/sink/config/PixelsSinkDefaultConfig.java b/src/main/java/io/pixelsdb/pixels/sink/config/PixelsSinkDefaultConfig.java
index 1a56ddd..585ce0a 100644
--- a/src/main/java/io/pixelsdb/pixels/sink/config/PixelsSinkDefaultConfig.java
+++ b/src/main/java/io/pixelsdb/pixels/sink/config/PixelsSinkDefaultConfig.java
@@ -1,28 +1,24 @@
/*
* Copyright 2025 PixelsDB.
*
- * This file is part of Pixels.
+ * Licensed 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
*
- * Pixels is free software: you can redistribute it and/or modify
- * it under the terms of the Affero GNU General Public License as
- * published by the Free Software Foundation, either version 3 of
- * the License, or (at your option) any later version.
+ * http://www.apache.org/licenses/LICENSE-2.0
*
- * Pixels is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * Affero GNU General Public License for more details.
- *
- * You should have received a copy of the Affero GNU General Public
- * License along with Pixels. If not, see
- * .
+ * 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 io.pixelsdb.pixels.sink.config;
public class PixelsSinkDefaultConfig
{
public static final String DATA_SOURCE = "engine";
+ public static final String SOURCE_DECODE_THREADS = "4";
public static final String PROPERTIES_PATH = "pixels-sink.properties";
public static final String CSV_SINK_PATH = "./data";
@@ -52,6 +48,6 @@ public class PixelsSinkDefaultConfig
// Mock RPC
public static final boolean SINK_RPC_ENABLED = true;
- public static final int MOCK_RPC_DELAY = 100;
public static final String MAX_RECORDS_PER_FILE = "100000";
+ public static final String STORAGE_MODE = "stream";
}
diff --git a/src/main/java/io/pixelsdb/pixels/sink/config/TransactionConfig.java b/src/main/java/io/pixelsdb/pixels/sink/config/TransactionConfig.java
index f516b3a..53585fe 100644
--- a/src/main/java/io/pixelsdb/pixels/sink/config/TransactionConfig.java
+++ b/src/main/java/io/pixelsdb/pixels/sink/config/TransactionConfig.java
@@ -1,31 +1,23 @@
/*
* Copyright 2025 PixelsDB.
*
- * This file is part of Pixels.
+ * Licensed 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
*
- * Pixels is free software: you can redistribute it and/or modify
- * it under the terms of the Affero GNU General Public License as
- * published by the Free Software Foundation, either version 3 of
- * the License, or (at your option) any later version.
+ * http://www.apache.org/licenses/LICENSE-2.0
*
- * Pixels is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * Affero GNU General Public License for more details.
- *
- * You should have received a copy of the Affero GNU General Public
- * License along with Pixels. If not, see
- * .
+ * 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 io.pixelsdb.pixels.sink.config;
-import io.pixelsdb.pixels.sink.event.deserializer.TransactionJsonMessageDeserializer;
-
public class TransactionConfig
{
public static final String DEFAULT_TRANSACTION_TOPIC_SUFFIX = "transaction";
- public static final String DEFAULT_TRANSACTION_TOPIC_VALUE_DESERIALIZER = TransactionJsonMessageDeserializer.class.getName();
public static final String DEFAULT_TRANSACTION_TOPIC_GROUP_ID = "transaction_consumer";
public static final String DEFAULT_TRANSACTION_TIME_OUT = "300";
diff --git a/src/main/java/io/pixelsdb/pixels/sink/config/factory/KafkaPropFactory.java b/src/main/java/io/pixelsdb/pixels/sink/config/factory/KafkaPropFactory.java
index cbace9f..e1eceb6 100644
--- a/src/main/java/io/pixelsdb/pixels/sink/config/factory/KafkaPropFactory.java
+++ b/src/main/java/io/pixelsdb/pixels/sink/config/factory/KafkaPropFactory.java
@@ -1,23 +1,18 @@
/*
- * Copyright 2025 PixelsDB.
+ * Copyright 2023 PixelsDB.
*
- * This file is part of Pixels.
+ * Licensed 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
*
- * Pixels is free software: you can redistribute it and/or modify
- * it under the terms of the Affero GNU General Public License as
- * published by the Free Software Foundation, either version 3 of
- * the License, or (at your option) any later version.
+ * http://www.apache.org/licenses/LICENSE-2.0
*
- * Pixels is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * Affero GNU General Public License for more details.
- *
- * You should have received a copy of the Affero GNU General Public
- * License along with Pixels. If not, see
- * .
+ * 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 io.pixelsdb.pixels.sink.config.factory;
import io.pixelsdb.pixels.sink.config.PixelsSinkConfig;
diff --git a/src/main/java/io/pixelsdb/pixels/sink/config/factory/KafkaPropFactorySelector.java b/src/main/java/io/pixelsdb/pixels/sink/config/factory/KafkaPropFactorySelector.java
index c0343f9..9d2431d 100644
--- a/src/main/java/io/pixelsdb/pixels/sink/config/factory/KafkaPropFactorySelector.java
+++ b/src/main/java/io/pixelsdb/pixels/sink/config/factory/KafkaPropFactorySelector.java
@@ -1,23 +1,18 @@
/*
* Copyright 2025 PixelsDB.
*
- * This file is part of Pixels.
+ * Licensed 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
*
- * Pixels is free software: you can redistribute it and/or modify
- * it under the terms of the Affero GNU General Public License as
- * published by the Free Software Foundation, either version 3 of
- * the License, or (at your option) any later version.
+ * http://www.apache.org/licenses/LICENSE-2.0
*
- * Pixels is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * Affero GNU General Public License for more details.
- *
- * You should have received a copy of the Affero GNU General Public
- * License along with Pixels. If not, see
- * .
+ * 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 io.pixelsdb.pixels.sink.config.factory;
import io.pixelsdb.pixels.sink.config.PixelsSinkConstants;
diff --git a/src/main/java/io/pixelsdb/pixels/sink/config/factory/PixelsSinkConfigFactory.java b/src/main/java/io/pixelsdb/pixels/sink/config/factory/PixelsSinkConfigFactory.java
index 56b3550..a25dfc3 100644
--- a/src/main/java/io/pixelsdb/pixels/sink/config/factory/PixelsSinkConfigFactory.java
+++ b/src/main/java/io/pixelsdb/pixels/sink/config/factory/PixelsSinkConfigFactory.java
@@ -1,23 +1,18 @@
/*
* Copyright 2025 PixelsDB.
*
- * This file is part of Pixels.
+ * Licensed 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
*
- * Pixels is free software: you can redistribute it and/or modify
- * it under the terms of the Affero GNU General Public License as
- * published by the Free Software Foundation, either version 3 of
- * the License, or (at your option) any later version.
+ * http://www.apache.org/licenses/LICENSE-2.0
*
- * Pixels is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * Affero GNU General Public License for more details.
- *
- * You should have received a copy of the Affero GNU General Public
- * License along with Pixels. If not, see
- * .
+ * 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 io.pixelsdb.pixels.sink.config.factory;
import io.pixelsdb.pixels.common.utils.ConfigFactory;
diff --git a/src/main/java/io/pixelsdb/pixels/sink/config/factory/RowRecordKafkaPropFactory.java b/src/main/java/io/pixelsdb/pixels/sink/config/factory/RowRecordKafkaPropFactory.java
index 6c1fbc0..be04556 100644
--- a/src/main/java/io/pixelsdb/pixels/sink/config/factory/RowRecordKafkaPropFactory.java
+++ b/src/main/java/io/pixelsdb/pixels/sink/config/factory/RowRecordKafkaPropFactory.java
@@ -1,27 +1,25 @@
/*
* Copyright 2025 PixelsDB.
*
- * This file is part of Pixels.
+ * Licensed 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
*
- * Pixels is free software: you can redistribute it and/or modify
- * it under the terms of the Affero GNU General Public License as
- * published by the Free Software Foundation, either version 3 of
- * the License, or (at your option) any later version.
+ * http://www.apache.org/licenses/LICENSE-2.0
*
- * Pixels is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * Affero GNU General Public License for more details.
- *
- * You should have received a copy of the Affero GNU General Public
- * License along with Pixels. If not, see
- * .
+ * 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 io.pixelsdb.pixels.sink.config.factory;
import io.pixelsdb.pixels.sink.config.PixelsSinkConfig;
+import io.pixelsdb.pixels.sink.config.PixelsSinkConstants;
+import io.apicurio.registry.serde.SerdeConfig;
import org.apache.kafka.clients.consumer.ConsumerConfig;
+import org.apache.kafka.common.serialization.ByteArrayDeserializer;
import java.util.Properties;
@@ -33,6 +31,8 @@ static Properties getCommonKafkaProperties(PixelsSinkConfig config)
kafkaProperties.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, config.getBootstrapServers());
kafkaProperties.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, config.getKeyDeserializer());
kafkaProperties.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, "earliest");
+ kafkaProperties.put(
+ PixelsSinkConstants.KAFKA_VALUE_FORMAT, config.getKafkaValueFormat());
return kafkaProperties;
}
@@ -40,7 +40,16 @@ static Properties getCommonKafkaProperties(PixelsSinkConfig config)
public Properties createKafkaProperties(PixelsSinkConfig config)
{
Properties kafkaProperties = getCommonKafkaProperties(config);
- kafkaProperties.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, config.getValueDeserializer());
+ String dialect = config.resolveDebeziumSourceDialect();
+ if (!dialect.isBlank())
+ {
+ kafkaProperties.put(PixelsSinkConstants.SINK_DEBEZIUM_DIALECT, dialect);
+ }
+ if (config.getRegistryUrl() != null && !config.getRegistryUrl().isBlank())
+ {
+ kafkaProperties.put(SerdeConfig.REGISTRY_URL, config.getRegistryUrl());
+ }
+ kafkaProperties.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, ByteArrayDeserializer.class);
kafkaProperties.put(ConsumerConfig.GROUP_ID_CONFIG, config.getGroupId());
kafkaProperties.put(ConsumerConfig.SESSION_TIMEOUT_MS_CONFIG, "30000");
diff --git a/src/main/java/io/pixelsdb/pixels/sink/config/factory/TransactionKafkaPropFactory.java b/src/main/java/io/pixelsdb/pixels/sink/config/factory/TransactionKafkaPropFactory.java
index 52949b7..464f162 100644
--- a/src/main/java/io/pixelsdb/pixels/sink/config/factory/TransactionKafkaPropFactory.java
+++ b/src/main/java/io/pixelsdb/pixels/sink/config/factory/TransactionKafkaPropFactory.java
@@ -1,27 +1,25 @@
/*
* Copyright 2025 PixelsDB.
*
- * This file is part of Pixels.
+ * Licensed 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
*
- * Pixels is free software: you can redistribute it and/or modify
- * it under the terms of the Affero GNU General Public License as
- * published by the Free Software Foundation, either version 3 of
- * the License, or (at your option) any later version.
+ * http://www.apache.org/licenses/LICENSE-2.0
*
- * Pixels is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * Affero GNU General Public License for more details.
- *
- * You should have received a copy of the Affero GNU General Public
- * License along with Pixels. If not, see
- * .
+ * 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 io.pixelsdb.pixels.sink.config.factory;
+import io.apicurio.registry.serde.SerdeConfig;
import io.pixelsdb.pixels.sink.config.PixelsSinkConfig;
+import io.pixelsdb.pixels.sink.config.PixelsSinkConstants;
import org.apache.kafka.clients.consumer.ConsumerConfig;
+import org.apache.kafka.common.serialization.ByteArrayDeserializer;
import java.util.Properties;
@@ -33,7 +31,16 @@ public class TransactionKafkaPropFactory implements KafkaPropFactory
public Properties createKafkaProperties(PixelsSinkConfig config)
{
Properties kafkaProperties = getCommonKafkaProperties(config);
- kafkaProperties.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, config.getTransactionTopicValueDeserializer());
+ String dialect = config.resolveDebeziumSourceDialect();
+ if (!dialect.isBlank())
+ {
+ kafkaProperties.put(PixelsSinkConstants.SINK_DEBEZIUM_DIALECT, dialect);
+ }
+ if (config.getRegistryUrl() != null && !config.getRegistryUrl().isBlank())
+ {
+ kafkaProperties.put(SerdeConfig.REGISTRY_URL, config.getRegistryUrl());
+ }
+ kafkaProperties.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, ByteArrayDeserializer.class);
kafkaProperties.put(ConsumerConfig.GROUP_ID_CONFIG, config.getTransactionTopicGroupId() + "-" + config.getGroupId());
return kafkaProperties;
}
diff --git a/src/test/java/io/pixelsdb/pixels/sink/TestSplitString.java b/src/main/java/io/pixelsdb/pixels/sink/conversion/debezium/DebeziumRowConverter.java
similarity index 57%
rename from src/test/java/io/pixelsdb/pixels/sink/TestSplitString.java
rename to src/main/java/io/pixelsdb/pixels/sink/conversion/debezium/DebeziumRowConverter.java
index 5d32ba9..138e737 100644
--- a/src/test/java/io/pixelsdb/pixels/sink/TestSplitString.java
+++ b/src/main/java/io/pixelsdb/pixels/sink/conversion/debezium/DebeziumRowConverter.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2025 PixelsDB.
+ * Copyright 2023 PixelsDB.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -12,24 +12,16 @@
* 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 io.pixelsdb.pixels.sink;
+package io.pixelsdb.pixels.sink.conversion.debezium;
-import org.junit.Test;
+import io.pixelsdb.pixels.sink.event.RowChangeEvent;
/**
- * Created at: 29/04/2021
- * Author: hank
+ * Converts a Debezium row envelope (any wire format) into a canonical row event.
*/
-public class TestSplitString
+@FunctionalInterface
+public interface DebeziumRowConverter
{
- @Test
- public void test()
- {
- String s = "1|3689999|O|224560.83|1996-01-02|5-LOW|Clerk#000095055|0|nstructions sleep furiously among |";
- String reg = "\\\\|";
- String[] splits = s.split(reg);
- System.out.println(splits.length);
- }
+ RowChangeEvent convert(I input) throws Exception;
}
diff --git a/src/main/java/io/pixelsdb/pixels/sink/conversion/debezium/DebeziumTransactionConverter.java b/src/main/java/io/pixelsdb/pixels/sink/conversion/debezium/DebeziumTransactionConverter.java
new file mode 100644
index 0000000..4b8893e
--- /dev/null
+++ b/src/main/java/io/pixelsdb/pixels/sink/conversion/debezium/DebeziumTransactionConverter.java
@@ -0,0 +1,27 @@
+/*
+ * Copyright 2023 PixelsDB.
+ *
+ * Licensed 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 io.pixelsdb.pixels.sink.conversion.debezium;
+
+import io.pixelsdb.pixels.sink.SinkProto;
+
+/**
+ * Converts a Debezium transaction envelope (any wire format) into metadata.
+ */
+@FunctionalInterface
+public interface DebeziumTransactionConverter
+{
+ SinkProto.TransactionMetadata convert(I input) throws Exception;
+}
diff --git a/src/main/java/io/pixelsdb/pixels/sink/conversion/debezium/avro/DebeziumAvroPayloadDecoder.java b/src/main/java/io/pixelsdb/pixels/sink/conversion/debezium/avro/DebeziumAvroPayloadDecoder.java
new file mode 100644
index 0000000..4b6924c
--- /dev/null
+++ b/src/main/java/io/pixelsdb/pixels/sink/conversion/debezium/avro/DebeziumAvroPayloadDecoder.java
@@ -0,0 +1,50 @@
+/*
+ * Copyright 2026 PixelsDB.
+ *
+ * Licensed 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 io.pixelsdb.pixels.sink.conversion.debezium.avro;
+
+import io.apicurio.registry.serde.SerdeConfig;
+import io.apicurio.registry.serde.avro.AvroKafkaDeserializer;
+import org.apache.avro.generic.GenericRecord;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Decodes Kafka Avro Confluent/Apicurio payloads to {@link GenericRecord}.
+ */
+public final class DebeziumAvroPayloadDecoder implements AutoCloseable
+{
+ private final AvroKafkaDeserializer avroDeserializer =
+ new AvroKafkaDeserializer<>();
+
+ public void configure(Map configs, boolean isKey)
+ {
+ Map enrichedConfig = new HashMap<>(configs);
+ enrichedConfig.put(SerdeConfig.CHECK_PERIOD_MS, SerdeConfig.CHECK_PERIOD_MS_DEFAULT);
+ avroDeserializer.configure(enrichedConfig, isKey);
+ }
+
+ public GenericRecord decode(String topic, byte[] data)
+ {
+ return avroDeserializer.deserialize(topic, data);
+ }
+
+ @Override
+ public void close()
+ {
+ avroDeserializer.close();
+ }
+}
diff --git a/src/main/java/io/pixelsdb/pixels/sink/conversion/debezium/avro/DebeziumAvroRowConverter.java b/src/main/java/io/pixelsdb/pixels/sink/conversion/debezium/avro/DebeziumAvroRowConverter.java
new file mode 100644
index 0000000..34aaf4e
--- /dev/null
+++ b/src/main/java/io/pixelsdb/pixels/sink/conversion/debezium/avro/DebeziumAvroRowConverter.java
@@ -0,0 +1,102 @@
+/*
+ * Copyright 2026 PixelsDB.
+ *
+ * Licensed 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 io.pixelsdb.pixels.sink.conversion.debezium.avro;
+
+import io.pixelsdb.pixels.core.TypeDescription;
+import io.pixelsdb.pixels.sink.SinkProto;
+import io.pixelsdb.pixels.sink.conversion.debezium.DebeziumRowConverter;
+import io.pixelsdb.pixels.sink.conversion.debezium.dialect.DebeziumSourceAdapter;
+import io.pixelsdb.pixels.sink.conversion.debezium.support.DebeziumEnvelopeNormalizer;
+import io.pixelsdb.pixels.sink.conversion.debezium.support.DebeziumRecordUtil;
+import io.pixelsdb.pixels.sink.conversion.debezium.support.DebeziumRowRecordAssembler;
+import io.pixelsdb.pixels.sink.conversion.debezium.support.DebeziumRowValueConverter;
+import io.pixelsdb.pixels.sink.event.RowChangeEvent;
+import io.pixelsdb.pixels.sink.exception.SinkException;
+import io.pixelsdb.pixels.sink.metadata.TableMetadata;
+import io.pixelsdb.pixels.sink.metadata.TableMetadataRegistry;
+import org.apache.avro.generic.GenericRecord;
+
+public final class DebeziumAvroRowConverter implements DebeziumRowConverter
+{
+ private final TableMetadataRegistry tableMetadataRegistry;
+ private final DebeziumSourceAdapter configuredAdapter;
+ private final DebeziumRowRecordAssembler rowRecordAssembler =
+ new DebeziumRowRecordAssembler();
+
+ public DebeziumAvroRowConverter(
+ TableMetadataRegistry tableMetadataRegistry,
+ DebeziumSourceAdapter configuredAdapter)
+ {
+ this.tableMetadataRegistry = tableMetadataRegistry;
+ this.configuredAdapter = configuredAdapter;
+ }
+
+ @Override
+ public RowChangeEvent convert(GenericRecord record) throws SinkException
+ {
+ SinkProto.OperationType operation = DebeziumRecordUtil.getOperationType(
+ DebeziumRecordUtil.getStringSafely(record, "op"));
+ Object sourceObject = record.get("source");
+ if (!(sourceObject instanceof GenericRecord source))
+ {
+ throw new SinkException("Missing source field in row record");
+ }
+ DebeziumSourceAdapter adapter = configuredAdapter == null
+ ? DebeziumEnvelopeNormalizer.adapterForSource(source)
+ : configuredAdapter;
+ SinkProto.SourceInfo sourceInfo =
+ DebeziumEnvelopeNormalizer.normalizeSource(source, adapter);
+ TableMetadata metadata = tableMetadataRegistry.getMetadata(
+ sourceInfo.getDb(), sourceInfo.getTable());
+ TypeDescription schema = metadata.getTypeDescription();
+ DebeziumRowValueConverter rowValueConverter =
+ new DebeziumRowValueConverter(schema);
+
+ SinkProto.RowValue before = parseRowValue(
+ record.get("before"), operation, true, rowValueConverter);
+ SinkProto.RowValue after = parseRowValue(
+ record.get("after"), operation, false, rowValueConverter);
+ SinkProto.TransactionInfo transaction = record.get("transaction") == null
+ ? null
+ : DebeziumEnvelopeNormalizer.normalizeTransaction(
+ record.get("transaction"), adapter);
+ return rowRecordAssembler.assemble(
+ operation, sourceInfo, transaction, before, after, schema, metadata);
+ }
+
+ private SinkProto.RowValue parseRowValue(
+ Object value,
+ SinkProto.OperationType operation,
+ boolean before,
+ DebeziumRowValueConverter converter) throws SinkException
+ {
+ boolean required = before
+ ? DebeziumRecordUtil.hasBeforeValue(operation)
+ : DebeziumRecordUtil.hasAfterValue(operation);
+ if (!required)
+ {
+ return null;
+ }
+ if (!(value instanceof GenericRecord row))
+ {
+ throw new SinkException(
+ "Missing " + (before ? "before" : "after") + " image for " + operation);
+ }
+ SinkProto.RowValue.Builder builder = SinkProto.RowValue.newBuilder();
+ converter.parse(row, builder);
+ return builder.build();
+ }
+}
diff --git a/src/main/java/io/pixelsdb/pixels/sink/conversion/debezium/avro/DebeziumAvroTransactionConverter.java b/src/main/java/io/pixelsdb/pixels/sink/conversion/debezium/avro/DebeziumAvroTransactionConverter.java
new file mode 100644
index 0000000..462e994
--- /dev/null
+++ b/src/main/java/io/pixelsdb/pixels/sink/conversion/debezium/avro/DebeziumAvroTransactionConverter.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright 2026 PixelsDB.
+ *
+ * Licensed 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 io.pixelsdb.pixels.sink.conversion.debezium.avro;
+
+import io.pixelsdb.pixels.sink.SinkProto;
+import io.pixelsdb.pixels.sink.conversion.debezium.DebeziumTransactionConverter;
+import io.pixelsdb.pixels.sink.conversion.debezium.dialect.DebeziumSourceAdapter;
+import io.pixelsdb.pixels.sink.conversion.debezium.support.DebeziumEnvelopeNormalizer;
+import org.apache.avro.generic.GenericRecord;
+
+public final class DebeziumAvroTransactionConverter
+ implements DebeziumTransactionConverter
+{
+ private final DebeziumSourceAdapter configuredAdapter;
+
+ public DebeziumAvroTransactionConverter(DebeziumSourceAdapter configuredAdapter)
+ {
+ this.configuredAdapter = configuredAdapter;
+ }
+
+ @Override
+ public SinkProto.TransactionMetadata convert(GenericRecord record)
+ {
+ return configuredAdapter == null
+ ? DebeziumEnvelopeNormalizer.normalizeTransactionMetadata(record)
+ : DebeziumEnvelopeNormalizer.normalizeTransactionMetadata(
+ record, configuredAdapter);
+ }
+}
diff --git a/src/main/java/io/pixelsdb/pixels/sink/conversion/debezium/connect/DebeziumConnectRowConverter.java b/src/main/java/io/pixelsdb/pixels/sink/conversion/debezium/connect/DebeziumConnectRowConverter.java
new file mode 100644
index 0000000..ebc61e6
--- /dev/null
+++ b/src/main/java/io/pixelsdb/pixels/sink/conversion/debezium/connect/DebeziumConnectRowConverter.java
@@ -0,0 +1,138 @@
+/*
+ * Copyright 2026 PixelsDB.
+ *
+ * Licensed 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 io.pixelsdb.pixels.sink.conversion.debezium.connect;
+
+import io.pixelsdb.pixels.core.TypeDescription;
+import io.pixelsdb.pixels.sink.SinkProto;
+import io.pixelsdb.pixels.sink.conversion.debezium.DebeziumRowConverter;
+import io.pixelsdb.pixels.sink.conversion.debezium.dialect.DebeziumSourceAdapter;
+import io.pixelsdb.pixels.sink.conversion.debezium.support.DebeziumEnvelopeNormalizer;
+import io.pixelsdb.pixels.sink.conversion.debezium.support.DebeziumRecordUtil;
+import io.pixelsdb.pixels.sink.conversion.debezium.support.DebeziumRowRecordAssembler;
+import io.pixelsdb.pixels.sink.conversion.debezium.support.DebeziumRowValueConverter;
+import io.pixelsdb.pixels.sink.event.RowChangeEvent;
+import io.pixelsdb.pixels.sink.exception.SinkException;
+import io.pixelsdb.pixels.sink.metadata.TableMetadata;
+import io.pixelsdb.pixels.sink.metadata.TableMetadataRegistry;
+import org.apache.kafka.connect.data.Struct;
+import org.apache.kafka.connect.errors.DataException;
+import org.apache.kafka.connect.source.SourceRecord;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Converts Kafka Connect {@link SourceRecord} Debezium envelopes to row events.
+ */
+public final class DebeziumConnectRowConverter implements DebeziumRowConverter
+{
+ private static final Logger LOGGER = LoggerFactory.getLogger(DebeziumConnectRowConverter.class);
+
+ private final TableMetadataRegistry tableMetadataRegistry;
+ private final DebeziumSourceAdapter configuredAdapter;
+ private final DebeziumRowRecordAssembler rowRecordAssembler = new DebeziumRowRecordAssembler();
+
+ public DebeziumConnectRowConverter(
+ TableMetadataRegistry tableMetadataRegistry,
+ DebeziumSourceAdapter configuredAdapter)
+ {
+ this.tableMetadataRegistry = tableMetadataRegistry;
+ this.configuredAdapter = configuredAdapter;
+ }
+
+ @Override
+ public RowChangeEvent convert(SourceRecord sourceRecord) throws SinkException
+ {
+ if (!(sourceRecord.value() instanceof Struct value))
+ {
+ throw new SinkException("Debezium row value must be a Struct");
+ }
+ String op = DebeziumRecordUtil.getStringSafely(value, "op");
+ SinkProto.OperationType operationType = DebeziumRecordUtil.getOperationType(op);
+ return buildRowRecord(value, operationType);
+ }
+
+ private RowChangeEvent buildRowRecord(
+ Struct value, SinkProto.OperationType opType) throws SinkException
+ {
+ DebeziumSourceAdapter adapter;
+ SinkProto.SourceInfo sourceInfo;
+ try
+ {
+ Struct source = value.getStruct("source");
+ if (source == null)
+ {
+ throw new SinkException("Missing source field in row record");
+ }
+ adapter = configuredAdapter == null
+ ? DebeziumEnvelopeNormalizer.adapterForSource(source)
+ : configuredAdapter;
+ sourceInfo = DebeziumEnvelopeNormalizer.normalizeSource(source, adapter);
+ } catch (DataException | IllegalArgumentException e)
+ {
+ LOGGER.warn("Missing source field in row record");
+ throw new SinkException(e);
+ }
+
+ TableMetadata metadata = tableMetadataRegistry.getMetadata(
+ sourceInfo.getDb(), sourceInfo.getTable());
+ TypeDescription typeDescription = metadata.getTypeDescription();
+ DebeziumRowValueConverter rowDataParser = new DebeziumRowValueConverter(typeDescription);
+
+ SinkProto.TransactionInfo transactionInfo = null;
+ try
+ {
+ Struct transaction = value.getStruct("transaction");
+ if (transaction != null)
+ {
+ transactionInfo = DebeziumEnvelopeNormalizer.normalizeTransaction(
+ transaction, adapter);
+ }
+ } catch (DataException e)
+ {
+ LOGGER.warn("Missing transaction field in row record");
+ }
+
+ SinkProto.RowValue beforeValue = null;
+ if (DebeziumRecordUtil.hasBeforeValue(opType))
+ {
+ Struct before = value.getStruct("before");
+ if (before == null)
+ {
+ throw new SinkException("Missing before image for " + opType);
+ }
+ SinkProto.RowValue.Builder beforeBuilder = SinkProto.RowValue.newBuilder();
+ rowDataParser.parse(before, beforeBuilder);
+ beforeValue = beforeBuilder.build();
+ }
+
+ SinkProto.RowValue afterValue = null;
+ if (DebeziumRecordUtil.hasAfterValue(opType))
+ {
+ Struct after = value.getStruct("after");
+ if (after == null)
+ {
+ throw new SinkException("Missing after image for " + opType);
+ }
+ SinkProto.RowValue.Builder afterBuilder = SinkProto.RowValue.newBuilder();
+ rowDataParser.parse(after, afterBuilder);
+ afterValue = afterBuilder.build();
+ }
+
+ return rowRecordAssembler.assemble(
+ opType, sourceInfo, transactionInfo, beforeValue, afterValue,
+ typeDescription, metadata);
+ }
+}
diff --git a/src/main/java/io/pixelsdb/pixels/sink/conversion/debezium/connect/DebeziumConnectTransactionConverter.java b/src/main/java/io/pixelsdb/pixels/sink/conversion/debezium/connect/DebeziumConnectTransactionConverter.java
new file mode 100644
index 0000000..461c2b0
--- /dev/null
+++ b/src/main/java/io/pixelsdb/pixels/sink/conversion/debezium/connect/DebeziumConnectTransactionConverter.java
@@ -0,0 +1,49 @@
+/*
+ * Copyright 2026 PixelsDB.
+ *
+ * Licensed 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 io.pixelsdb.pixels.sink.conversion.debezium.connect;
+
+import io.pixelsdb.pixels.sink.SinkProto;
+import io.pixelsdb.pixels.sink.conversion.debezium.DebeziumTransactionConverter;
+import io.pixelsdb.pixels.sink.conversion.debezium.dialect.DebeziumSourceAdapter;
+import io.pixelsdb.pixels.sink.conversion.debezium.support.DebeziumEnvelopeNormalizer;
+import org.apache.kafka.connect.data.Struct;
+import org.apache.kafka.connect.source.SourceRecord;
+
+/**
+ * Converts Kafka Connect {@link SourceRecord} transaction envelopes.
+ */
+public final class DebeziumConnectTransactionConverter
+ implements DebeziumTransactionConverter
+{
+ private final DebeziumSourceAdapter configuredAdapter;
+
+ public DebeziumConnectTransactionConverter(DebeziumSourceAdapter configuredAdapter)
+ {
+ this.configuredAdapter = configuredAdapter;
+ }
+
+ @Override
+ public SinkProto.TransactionMetadata convert(SourceRecord sourceRecord)
+ {
+ if (!(sourceRecord.value() instanceof Struct value))
+ {
+ throw new IllegalArgumentException("Debezium transaction value must be a Struct");
+ }
+ return configuredAdapter == null
+ ? DebeziumEnvelopeNormalizer.normalizeTransactionMetadata(value)
+ : DebeziumEnvelopeNormalizer.normalizeTransactionMetadata(value, configuredAdapter);
+ }
+}
diff --git a/src/main/java/io/pixelsdb/pixels/sink/conversion/debezium/dialect/DebeziumSourceAdapter.java b/src/main/java/io/pixelsdb/pixels/sink/conversion/debezium/dialect/DebeziumSourceAdapter.java
new file mode 100644
index 0000000..d64eca8
--- /dev/null
+++ b/src/main/java/io/pixelsdb/pixels/sink/conversion/debezium/dialect/DebeziumSourceAdapter.java
@@ -0,0 +1,44 @@
+/*
+ * Copyright 2026 PixelsDB.
+ *
+ * Licensed 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 io.pixelsdb.pixels.sink.conversion.debezium.dialect;
+
+import io.pixelsdb.pixels.sink.SinkProto;
+
+import java.util.Locale;
+
+public interface DebeziumSourceAdapter
+{
+ String connector();
+
+ default boolean supports(String connectorName)
+ {
+ if (connectorName == null || connectorName.isBlank())
+ {
+ return false;
+ }
+ String normalized = connectorName.toLowerCase(Locale.ROOT);
+ return normalized.equals(connector()) || normalized.contains(connector());
+ }
+
+ String normalizeTransactionId(String sourceId);
+
+ SinkProto.SourceInfo normalizeSource(DebeziumSourceMetadata source);
+
+ default String normalizeDataCollection(String sourceDataCollection)
+ {
+ return sourceDataCollection;
+ }
+}
diff --git a/src/main/java/io/pixelsdb/pixels/sink/conversion/debezium/dialect/DebeziumSourceAdapterRegistry.java b/src/main/java/io/pixelsdb/pixels/sink/conversion/debezium/dialect/DebeziumSourceAdapterRegistry.java
new file mode 100644
index 0000000..b002962
--- /dev/null
+++ b/src/main/java/io/pixelsdb/pixels/sink/conversion/debezium/dialect/DebeziumSourceAdapterRegistry.java
@@ -0,0 +1,58 @@
+/*
+ * Copyright 2026 PixelsDB.
+ *
+ * Licensed 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 io.pixelsdb.pixels.sink.conversion.debezium.dialect;
+
+import java.util.List;
+import java.util.ServiceLoader;
+
+public final class DebeziumSourceAdapterRegistry
+{
+ private static final List ADAPTERS = ServiceLoader
+ .load(DebeziumSourceAdapter.class)
+ .stream()
+ .map(ServiceLoader.Provider::get)
+ .toList();
+
+ private DebeziumSourceAdapterRegistry()
+ {
+ }
+
+ public static DebeziumSourceAdapter forSource(String connector)
+ {
+ return resolve(connector);
+ }
+
+ public static DebeziumSourceAdapter resolve(String connector)
+ {
+ if (connector == null || connector.isBlank())
+ {
+ throw new IllegalArgumentException("Unsupported Debezium connector: " + connector);
+ }
+
+ List matches = ADAPTERS.stream()
+ .filter(adapter -> adapter.supports(connector))
+ .toList();
+ if (matches.size() == 1)
+ {
+ return matches.get(0);
+ }
+ if (matches.size() > 1)
+ {
+ throw new IllegalStateException("Multiple Debezium connector adapters support: " + connector);
+ }
+ throw new IllegalArgumentException("Unsupported Debezium connector: " + connector);
+ }
+}
diff --git a/src/main/java/io/pixelsdb/pixels/sink/conversion/debezium/dialect/DebeziumSourceMetadata.java b/src/main/java/io/pixelsdb/pixels/sink/conversion/debezium/dialect/DebeziumSourceMetadata.java
new file mode 100644
index 0000000..eda02f0
--- /dev/null
+++ b/src/main/java/io/pixelsdb/pixels/sink/conversion/debezium/dialect/DebeziumSourceMetadata.java
@@ -0,0 +1,24 @@
+/*
+ * Copyright 2026 PixelsDB.
+ *
+ * Licensed 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 io.pixelsdb.pixels.sink.conversion.debezium.dialect;
+
+public record DebeziumSourceMetadata(
+ String connector,
+ String db,
+ String schema,
+ String table)
+{
+}
diff --git a/src/main/java/io/pixelsdb/pixels/sink/conversion/debezium/dialect/MySqlSourceAdapter.java b/src/main/java/io/pixelsdb/pixels/sink/conversion/debezium/dialect/MySqlSourceAdapter.java
new file mode 100644
index 0000000..f35615e
--- /dev/null
+++ b/src/main/java/io/pixelsdb/pixels/sink/conversion/debezium/dialect/MySqlSourceAdapter.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright 2026 PixelsDB.
+ *
+ * Licensed 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 io.pixelsdb.pixels.sink.conversion.debezium.dialect;
+
+import io.pixelsdb.pixels.sink.SinkProto;
+
+public final class MySqlSourceAdapter implements DebeziumSourceAdapter
+{
+ public static final MySqlSourceAdapter INSTANCE = new MySqlSourceAdapter();
+
+ public MySqlSourceAdapter()
+ {
+ }
+
+ @Override
+ public String connector()
+ {
+ return "mysql";
+ }
+
+ @Override
+ public String normalizeTransactionId(String sourceId)
+ {
+ return sourceId;
+ }
+
+ @Override
+ public SinkProto.SourceInfo normalizeSource(DebeziumSourceMetadata source)
+ {
+ return SinkProto.SourceInfo.newBuilder()
+ .setDb(source.db())
+ .setTable(source.table())
+ .build();
+ }
+}
diff --git a/src/main/java/io/pixelsdb/pixels/sink/conversion/debezium/dialect/PostgresSourceAdapter.java b/src/main/java/io/pixelsdb/pixels/sink/conversion/debezium/dialect/PostgresSourceAdapter.java
new file mode 100644
index 0000000..77d6a0f
--- /dev/null
+++ b/src/main/java/io/pixelsdb/pixels/sink/conversion/debezium/dialect/PostgresSourceAdapter.java
@@ -0,0 +1,54 @@
+/*
+ * Copyright 2026 PixelsDB.
+ *
+ * Licensed 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 io.pixelsdb.pixels.sink.conversion.debezium.dialect;
+
+import io.pixelsdb.pixels.sink.SinkProto;
+
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+public final class PostgresSourceAdapter implements DebeziumSourceAdapter
+{
+ public static final PostgresSourceAdapter INSTANCE = new PostgresSourceAdapter();
+ private static final Pattern TRANSACTION_WITH_LSN = Pattern.compile("^(\\d+):\\d+$");
+
+ public PostgresSourceAdapter()
+ {
+ }
+
+ @Override
+ public String connector()
+ {
+ return "postgresql";
+ }
+
+ @Override
+ public String normalizeTransactionId(String sourceId)
+ {
+ Matcher matcher = TRANSACTION_WITH_LSN.matcher(sourceId);
+ return matcher.matches() ? matcher.group(1) : sourceId;
+ }
+
+ @Override
+ public SinkProto.SourceInfo normalizeSource(DebeziumSourceMetadata source)
+ {
+ return SinkProto.SourceInfo.newBuilder()
+ .setDb(source.db())
+ .setSchema(source.schema())
+ .setTable(source.table())
+ .build();
+ }
+}
diff --git a/src/main/java/io/pixelsdb/pixels/sink/conversion/debezium/json/DebeziumJsonRowConverter.java b/src/main/java/io/pixelsdb/pixels/sink/conversion/debezium/json/DebeziumJsonRowConverter.java
new file mode 100644
index 0000000..d482131
--- /dev/null
+++ b/src/main/java/io/pixelsdb/pixels/sink/conversion/debezium/json/DebeziumJsonRowConverter.java
@@ -0,0 +1,124 @@
+/*
+ * Copyright 2026 PixelsDB.
+ *
+ * Licensed 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 io.pixelsdb.pixels.sink.conversion.debezium.json;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import io.pixelsdb.pixels.core.TypeDescription;
+import io.pixelsdb.pixels.sink.SinkProto;
+import io.pixelsdb.pixels.sink.conversion.debezium.DebeziumRowConverter;
+import io.pixelsdb.pixels.sink.conversion.debezium.dialect.DebeziumSourceAdapter;
+import io.pixelsdb.pixels.sink.conversion.debezium.support.DebeziumEnvelopeNormalizer;
+import io.pixelsdb.pixels.sink.conversion.debezium.support.DebeziumRecordUtil;
+import io.pixelsdb.pixels.sink.conversion.debezium.support.DebeziumRowRecordAssembler;
+import io.pixelsdb.pixels.sink.conversion.debezium.support.DebeziumRowValueConverter;
+import io.pixelsdb.pixels.sink.event.RowChangeEvent;
+import io.pixelsdb.pixels.sink.exception.SinkException;
+import io.pixelsdb.pixels.sink.metadata.TableMetadata;
+import io.pixelsdb.pixels.sink.metadata.TableMetadataRegistry;
+
+public final class DebeziumJsonRowConverter implements DebeziumRowConverter
+{
+ private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
+
+ private final TableMetadataRegistry tableMetadataRegistry;
+ private final DebeziumSourceAdapter configuredAdapter;
+ private final DebeziumRowRecordAssembler rowRecordAssembler =
+ new DebeziumRowRecordAssembler();
+
+ public DebeziumJsonRowConverter(
+ TableMetadataRegistry tableMetadataRegistry,
+ DebeziumSourceAdapter configuredAdapter)
+ {
+ this.tableMetadataRegistry = tableMetadataRegistry;
+ this.configuredAdapter = configuredAdapter;
+ }
+
+ @Override
+ public RowChangeEvent convert(byte[] data) throws Exception
+ {
+ JsonNode rootNode = OBJECT_MAPPER.readTree(data);
+ JsonNode payloadNode = rootNode.path("payload");
+ SinkProto.OperationType operation = DebeziumRecordUtil.getOperationType(
+ payloadNode.path("op").asText(""));
+
+ if (!payloadNode.hasNonNull("source"))
+ {
+ throw new SinkException("Missing source field in row record");
+ }
+
+ JsonNode sourceNode = payloadNode.get("source");
+ DebeziumSourceAdapter adapter = configuredAdapter == null
+ ? DebeziumEnvelopeNormalizer.adapterForSource(sourceNode)
+ : configuredAdapter;
+ SinkProto.SourceInfo sourceInfo =
+ DebeziumEnvelopeNormalizer.normalizeSource(sourceNode, adapter);
+ TableMetadata metadata = tableMetadataRegistry.getMetadata(
+ sourceInfo.getDb(), sourceInfo.getTable());
+ TypeDescription schema = metadata.getTypeDescription();
+ DebeziumRowValueConverter rowValueConverter =
+ new DebeziumRowValueConverter(schema);
+
+ SinkProto.TransactionInfo transaction = payloadNode.hasNonNull("transaction")
+ ? DebeziumEnvelopeNormalizer.normalizeTransaction(
+ payloadNode.get("transaction"), adapter)
+ : null;
+ SinkProto.RowValue before = parseBefore(
+ payloadNode, operation, rowValueConverter);
+ SinkProto.RowValue after = parseAfter(
+ payloadNode, operation, rowValueConverter);
+ return rowRecordAssembler.assemble(
+ operation, sourceInfo, transaction, before, after, schema, metadata);
+ }
+
+ private SinkProto.RowValue parseBefore(
+ JsonNode payload,
+ SinkProto.OperationType operation,
+ DebeziumRowValueConverter converter) throws SinkException
+ {
+ if (!DebeziumRecordUtil.hasBeforeValue(operation))
+ {
+ return null;
+ }
+ JsonNode before = payload.get("before");
+ if (before == null || before.isNull())
+ {
+ throw new SinkException("Missing before image for " + operation);
+ }
+ SinkProto.RowValue.Builder builder = SinkProto.RowValue.newBuilder();
+ converter.parse(before, builder);
+ return builder.build();
+ }
+
+ private SinkProto.RowValue parseAfter(
+ JsonNode payload,
+ SinkProto.OperationType operation,
+ DebeziumRowValueConverter converter) throws SinkException
+ {
+ if (!DebeziumRecordUtil.hasAfterValue(operation))
+ {
+ return null;
+ }
+ JsonNode after = payload.get("after");
+ if (after == null || after.isNull())
+ {
+ throw new SinkException("Missing after image for " + operation);
+ }
+ SinkProto.RowValue.Builder builder = SinkProto.RowValue.newBuilder();
+ converter.parse(after, builder);
+ return builder.build();
+ }
+}
diff --git a/src/main/java/io/pixelsdb/pixels/sink/conversion/debezium/json/DebeziumJsonTransactionConverter.java b/src/main/java/io/pixelsdb/pixels/sink/conversion/debezium/json/DebeziumJsonTransactionConverter.java
new file mode 100644
index 0000000..0b5da07
--- /dev/null
+++ b/src/main/java/io/pixelsdb/pixels/sink/conversion/debezium/json/DebeziumJsonTransactionConverter.java
@@ -0,0 +1,52 @@
+/*
+ * Copyright 2026 PixelsDB.
+ *
+ * Licensed 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 io.pixelsdb.pixels.sink.conversion.debezium.json;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import io.pixelsdb.pixels.sink.SinkProto;
+import io.pixelsdb.pixels.sink.conversion.debezium.DebeziumTransactionConverter;
+import io.pixelsdb.pixels.sink.conversion.debezium.dialect.DebeziumSourceAdapter;
+import io.pixelsdb.pixels.sink.conversion.debezium.support.DebeziumEnvelopeNormalizer;
+
+/**
+ * Converts Debezium JSON transaction envelopes ({@code byte[]} payload).
+ */
+public final class DebeziumJsonTransactionConverter
+ implements DebeziumTransactionConverter
+{
+ private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
+
+ private final DebeziumSourceAdapter configuredAdapter;
+
+ public DebeziumJsonTransactionConverter(DebeziumSourceAdapter configuredAdapter)
+ {
+ this.configuredAdapter = configuredAdapter;
+ }
+
+ @Override
+ public SinkProto.TransactionMetadata convert(byte[] data) throws Exception
+ {
+ if (configuredAdapter == null)
+ {
+ throw new IllegalStateException(
+ "No Debezium source adapter configured for JSON transaction conversion");
+ }
+ JsonNode payload = OBJECT_MAPPER.readTree(data).path("payload");
+ return DebeziumEnvelopeNormalizer.normalizeTransactionMetadata(
+ payload, configuredAdapter);
+ }
+}
diff --git a/src/main/java/io/pixelsdb/pixels/sink/conversion/debezium/support/DebeziumEnvelopeNormalizer.java b/src/main/java/io/pixelsdb/pixels/sink/conversion/debezium/support/DebeziumEnvelopeNormalizer.java
new file mode 100644
index 0000000..960b4a5
--- /dev/null
+++ b/src/main/java/io/pixelsdb/pixels/sink/conversion/debezium/support/DebeziumEnvelopeNormalizer.java
@@ -0,0 +1,118 @@
+/*
+ * Copyright 2026 PixelsDB.
+ *
+ * Licensed 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 io.pixelsdb.pixels.sink.conversion.debezium.support;
+
+import io.pixelsdb.pixels.sink.SinkProto;
+import io.pixelsdb.pixels.sink.conversion.debezium.dialect.DebeziumSourceAdapter;
+import io.pixelsdb.pixels.sink.conversion.debezium.dialect.DebeziumSourceAdapterRegistry;
+import io.pixelsdb.pixels.sink.conversion.debezium.dialect.DebeziumSourceMetadata;
+
+public final class DebeziumEnvelopeNormalizer
+{
+ private DebeziumEnvelopeNormalizer()
+ {
+ }
+
+ public static DebeziumSourceMetadata extractSource(Object source)
+ {
+ return new DebeziumSourceMetadata(
+ DebeziumRecordUtil.getStringSafely(source, "connector"),
+ DebeziumRecordUtil.getStringSafely(source, "db"),
+ DebeziumRecordUtil.getStringSafely(source, "schema"),
+ DebeziumRecordUtil.getStringSafely(source, "table"));
+ }
+
+ public static DebeziumSourceAdapter adapterForSource(Object source)
+ {
+ String payloadConnector = DebeziumRecordUtil.getStringSafely(source, "connector");
+ return DebeziumSourceAdapterRegistry.forSource(payloadConnector);
+ }
+
+ public static DebeziumSourceAdapter adapterForTransaction(Object transaction)
+ {
+ Object source = DebeziumRecordUtil.getFieldSafely(transaction, "source");
+ if (source == null)
+ {
+ throw new IllegalArgumentException(
+ "Transaction metadata requires an explicit source adapter");
+ }
+ return adapterForSource(source);
+ }
+
+ public static SinkProto.SourceInfo normalizeSource(Object source)
+ {
+ return normalizeSource(source, adapterForSource(source));
+ }
+
+ public static SinkProto.SourceInfo normalizeSource(
+ Object source, DebeziumSourceAdapter adapter)
+ {
+ DebeziumSourceMetadata metadata = extractSource(source);
+ if (!metadata.connector().isBlank() &&
+ !adapter.connector().equalsIgnoreCase(metadata.connector()))
+ {
+ throw new IllegalArgumentException(
+ "Configured Debezium connector " + adapter.connector() +
+ " does not match source.connector " + metadata.connector());
+ }
+
+ return adapter.normalizeSource(metadata);
+ }
+
+ public static SinkProto.TransactionInfo normalizeTransaction(
+ Object transaction, DebeziumSourceAdapter adapter)
+ {
+ String rawTransactionId = DebeziumRecordUtil.getStringSafely(transaction, "id");
+ return SinkProto.TransactionInfo.newBuilder()
+ .setId(adapter.normalizeTransactionId(rawTransactionId))
+ .setTotalOrder(DebeziumRecordUtil.getLongSafely(transaction, "total_order"))
+ .setDataCollectionOrder(
+ DebeziumRecordUtil.getLongSafely(transaction, "data_collection_order"))
+ .build();
+ }
+
+ public static SinkProto.TransactionMetadata normalizeTransactionMetadata(
+ Object transaction, DebeziumSourceAdapter adapter)
+ {
+ String rawTransactionId = DebeziumRecordUtil.getStringSafely(transaction, "id");
+ SinkProto.TransactionMetadata.Builder builder = SinkProto.TransactionMetadata.newBuilder()
+ .setStatus(DebeziumRecordUtil.getStatusSafely(transaction, "status"))
+ .setId(adapter.normalizeTransactionId(rawTransactionId))
+ .setEventCount(DebeziumRecordUtil.getLongSafely(transaction, "event_count"))
+ .setTimestamp(DebeziumRecordUtil.getLongSafely(transaction, "ts_ms"));
+
+ Object collections = DebeziumRecordUtil.getFieldSafely(transaction, "data_collections");
+ if (collections instanceof Iterable> iterable)
+ {
+ for (Object item : iterable)
+ {
+ String sourceDataCollection =
+ DebeziumRecordUtil.getStringSafely(item, "data_collection");
+ builder.addDataCollections(SinkProto.DataCollection.newBuilder()
+ .setDataCollection(
+ adapter.normalizeDataCollection(sourceDataCollection))
+ .setEventCount(
+ DebeziumRecordUtil.getLongSafely(item, "event_count")));
+ }
+ }
+ return builder.build();
+ }
+
+ public static SinkProto.TransactionMetadata normalizeTransactionMetadata(Object transaction)
+ {
+ return normalizeTransactionMetadata(transaction, adapterForTransaction(transaction));
+ }
+}
diff --git a/src/main/java/io/pixelsdb/pixels/sink/event/deserializer/DeserializerUtil.java b/src/main/java/io/pixelsdb/pixels/sink/conversion/debezium/support/DebeziumRecordUtil.java
similarity index 57%
rename from src/main/java/io/pixelsdb/pixels/sink/event/deserializer/DeserializerUtil.java
rename to src/main/java/io/pixelsdb/pixels/sink/conversion/debezium/support/DebeziumRecordUtil.java
index 81ef25d..75eaac2 100644
--- a/src/main/java/io/pixelsdb/pixels/sink/event/deserializer/DeserializerUtil.java
+++ b/src/main/java/io/pixelsdb/pixels/sink/conversion/debezium/support/DebeziumRecordUtil.java
@@ -1,33 +1,33 @@
/*
* Copyright 2025 PixelsDB.
*
- * This file is part of Pixels.
+ * Licensed 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
*
- * Pixels is free software: you can redistribute it and/or modify
- * it under the terms of the Affero GNU General Public License as
- * published by the Free Software Foundation, either version 3 of
- * the License, or (at your option) any later version.
+ * http://www.apache.org/licenses/LICENSE-2.0
*
- * Pixels is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * Affero GNU General Public License for more details.
- *
- * You should have received a copy of the Affero GNU General Public
- * License along with Pixels. If not, see
- * .
+ * 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 io.pixelsdb.pixels.sink.conversion.debezium.support;
-package io.pixelsdb.pixels.sink.event.deserializer;
-
+import com.fasterxml.jackson.databind.JsonNode;
import io.pixelsdb.pixels.sink.SinkProto;
import org.apache.avro.generic.GenericRecord;
import org.apache.kafka.connect.data.Struct;
import org.apache.kafka.connect.source.SourceRecord;
-public class DeserializerUtil
+public final class DebeziumRecordUtil
{
- static public SinkProto.TransactionStatus getStatusSafely(T record, String field)
+ private DebeziumRecordUtil()
+ {
+ }
+
+ public static SinkProto.TransactionStatus getStatusSafely(T record, String field)
{
String statusString = getStringSafely(record, field);
if (statusString.equals("BEGIN"))
@@ -52,6 +52,10 @@ public static Object getFieldSafely(T record, String field)
} else if (record instanceof Struct struct)
{
return struct.get(field);
+ } else if (record instanceof JsonNode json)
+ {
+ JsonNode value = json.get(field);
+ return value == null || value.isNull() ? null : value;
} else if (record instanceof SourceRecord sourceRecord)
{
return ((Struct) sourceRecord.value()).get(field);
@@ -66,49 +70,53 @@ public static Object getFieldSafely(T record, String field)
public static String getStringSafely(T record, String field)
{
Object value = getFieldSafely(record, field);
+ if (value instanceof JsonNode json)
+ {
+ return json.asText("");
+ }
return value != null ? value.toString() : "";
}
public static Long getLongSafely(T record, String field)
{
Object value = getFieldSafely(record, field);
+ if (value instanceof JsonNode json)
+ {
+ return json.asLong();
+ }
return value instanceof Number ? ((Number) value).longValue() : 0L;
}
public static Integer getIntSafely(T record, String field)
{
Object value = getFieldSafely(record, field);
+ if (value instanceof JsonNode json)
+ {
+ return json.asInt();
+ }
return value instanceof Number ? ((Number) value).intValue() : 0;
}
- static public SinkProto.OperationType getOperationType(String op)
+ public static SinkProto.OperationType getOperationType(String op)
{
- op = op.toLowerCase();
- return switch (op)
+ return switch (op.toLowerCase())
{
case "c" -> SinkProto.OperationType.INSERT;
case "u" -> SinkProto.OperationType.UPDATE;
case "d" -> SinkProto.OperationType.DELETE;
case "r" -> SinkProto.OperationType.SNAPSHOT;
- default -> throw new IllegalArgumentException(String.format("Can't convert %s to operation type", op));
+ default -> throw new IllegalArgumentException(
+ String.format("Can't convert %s to operation type", op));
};
}
- static public boolean hasBeforeValue(SinkProto.OperationType op)
+ public static boolean hasBeforeValue(SinkProto.OperationType op)
{
return op == SinkProto.OperationType.DELETE || op == SinkProto.OperationType.UPDATE;
}
- static public boolean hasAfterValue(SinkProto.OperationType op)
+ public static boolean hasAfterValue(SinkProto.OperationType op)
{
return op != SinkProto.OperationType.DELETE;
}
-
- static public String getTransIdPrefix(String originTransID)
- {
- return originTransID.contains(":")
- ? originTransID.substring(0, originTransID.indexOf(":"))
- : originTransID;
- }
-
}
diff --git a/src/main/java/io/pixelsdb/pixels/sink/conversion/debezium/support/DebeziumRowRecordAssembler.java b/src/main/java/io/pixelsdb/pixels/sink/conversion/debezium/support/DebeziumRowRecordAssembler.java
new file mode 100644
index 0000000..8aefa8e
--- /dev/null
+++ b/src/main/java/io/pixelsdb/pixels/sink/conversion/debezium/support/DebeziumRowRecordAssembler.java
@@ -0,0 +1,66 @@
+/*
+ * Copyright 2026 PixelsDB.
+ *
+ * Licensed 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 io.pixelsdb.pixels.sink.conversion.debezium.support;
+
+import io.pixelsdb.pixels.core.TypeDescription;
+import io.pixelsdb.pixels.sink.SinkProto;
+import io.pixelsdb.pixels.sink.event.RowChangeEvent;
+import io.pixelsdb.pixels.sink.event.RowChangeEventFactory;
+import io.pixelsdb.pixels.sink.exception.SinkException;
+import io.pixelsdb.pixels.sink.metadata.TableMetadata;
+
+public final class DebeziumRowRecordAssembler
+{
+ public RowChangeEvent assemble(
+ SinkProto.OperationType operation,
+ SinkProto.SourceInfo source,
+ SinkProto.TransactionInfo transaction,
+ SinkProto.RowValue before,
+ SinkProto.RowValue after,
+ TypeDescription schema) throws SinkException
+ {
+ return assemble(operation, source, transaction, before, after, schema, null);
+ }
+
+ public RowChangeEvent assemble(
+ SinkProto.OperationType operation,
+ SinkProto.SourceInfo source,
+ SinkProto.TransactionInfo transaction,
+ SinkProto.RowValue before,
+ SinkProto.RowValue after,
+ TypeDescription schema,
+ TableMetadata metadata) throws SinkException
+ {
+ SinkProto.RowRecord.Builder builder = SinkProto.RowRecord.newBuilder()
+ .setOp(operation)
+ .setSource(source);
+ if (transaction != null)
+ {
+ builder.setTransaction(transaction);
+ }
+ if (before != null)
+ {
+ builder.setBefore(before);
+ }
+ if (after != null)
+ {
+ builder.setAfter(after);
+ }
+ return metadata == null
+ ? RowChangeEventFactory.create(builder.build(), schema)
+ : RowChangeEventFactory.create(builder.build(), schema, metadata);
+ }
+}
diff --git a/src/main/java/io/pixelsdb/pixels/sink/event/deserializer/RowDataParser.java b/src/main/java/io/pixelsdb/pixels/sink/conversion/debezium/support/DebeziumRowValueConverter.java
similarity index 50%
rename from src/main/java/io/pixelsdb/pixels/sink/event/deserializer/RowDataParser.java
rename to src/main/java/io/pixelsdb/pixels/sink/conversion/debezium/support/DebeziumRowValueConverter.java
index 5c85d0a..6297e57 100644
--- a/src/main/java/io/pixelsdb/pixels/sink/event/deserializer/RowDataParser.java
+++ b/src/main/java/io/pixelsdb/pixels/sink/conversion/debezium/support/DebeziumRowValueConverter.java
@@ -1,49 +1,38 @@
/*
* Copyright 2025 PixelsDB.
*
- * This file is part of Pixels.
+ * Licensed 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
*
- * Pixels is free software: you can redistribute it and/or modify
- * it under the terms of the Affero GNU General Public License as
- * published by the Free Software Foundation, either version 3 of
- * the License, or (at your option) any later version.
+ * http://www.apache.org/licenses/LICENSE-2.0
*
- * Pixels is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * Affero GNU General Public License for more details.
- *
- * You should have received a copy of the Affero GNU General Public
- * License along with Pixels. If not, see
- * .
+ * 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 io.pixelsdb.pixels.sink.event.deserializer;
+package io.pixelsdb.pixels.sink.conversion.debezium.support;
import com.fasterxml.jackson.databind.JsonNode;
import com.google.protobuf.ByteString;
import io.pixelsdb.pixels.core.TypeDescription;
import io.pixelsdb.pixels.sink.SinkProto;
import org.apache.avro.generic.GenericRecord;
-import org.apache.kafka.connect.data.Field;
-import org.apache.kafka.connect.data.Schema;
import org.apache.kafka.connect.data.Struct;
-import java.io.IOException;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
-import java.time.LocalDate;
import java.util.Base64;
-import java.util.Collections;
-import java.util.Map;
-class RowDataParser
+public class DebeziumRowValueConverter
{
private final TypeDescription schema;
- public RowDataParser(TypeDescription schema)
+ public DebeziumRowValueConverter(TypeDescription schema)
{
this.schema = schema;
}
@@ -55,8 +44,15 @@ private static void buildFloat32(float value, SinkProto.ColumnValue.Builder colu
private static void buildInt32(int value, int capacity, SinkProto.ColumnValue.Builder columnValueBuilder)
{
- int intBits = value;
- byte[] bytes = ByteBuffer.allocate(capacity).putInt(intBits).array();
+ ByteBuffer buffer = ByteBuffer.allocate(capacity);
+ switch (capacity)
+ {
+ case Byte.BYTES -> buffer.put((byte) value);
+ case Short.BYTES -> buffer.putShort((short) value);
+ case Integer.BYTES -> buffer.putInt(value);
+ default -> throw new IllegalArgumentException("Unsupported integer width: " + capacity);
+ }
+ byte[] bytes = buffer.array();
columnValueBuilder.setValue(ByteString.copyFrom(bytes));
}
@@ -66,7 +62,7 @@ public void parse(GenericRecord record, SinkProto.RowValue.Builder builder)
{
String fieldName = schema.getFieldNames().get(i);
TypeDescription fieldType = schema.getChildren().get(i);
- builder.addValues(parseValue(record, fieldName, fieldType).build());
+ builder.addValues(parseCanonicalValue(record.get(fieldName), fieldType).build());
}
}
@@ -85,9 +81,12 @@ public void parse(Struct record, SinkProto.RowValue.Builder builder)
for (int i = 0; i < schema.getFieldNames().size(); i++)
{
String fieldName = schema.getFieldNames().get(i);
- Field field = record.schema().field(fieldName);
- Schema.Type fieldType = field.schema().type();
- builder.addValues(parseValue(record.get(fieldName), fieldName, fieldType).build());
+ TypeDescription fieldType = schema.getChildren().get(i);
+ if (record.schema().field(fieldName) == null)
+ {
+ throw new IllegalArgumentException("Missing field in Debezium row schema: " + fieldName);
+ }
+ builder.addValues(parseCanonicalValue(record.get(fieldName), fieldType).build());
}
}
@@ -104,6 +103,16 @@ private SinkProto.ColumnValue.Builder parseValue(JsonNode valueNode, String fiel
switch (type.getCategory())
{
+ case BYTE:
+ {
+ buildInt32(valueNode.asInt(), Byte.BYTES, columnValueBuilder);
+ break;
+ }
+ case SHORT:
+ {
+ buildInt32(valueNode.asInt(), Short.BYTES, columnValueBuilder);
+ break;
+ }
case INT:
{
buildInt32(valueNode.asInt(), Integer.BYTES, columnValueBuilder);
@@ -131,7 +140,7 @@ private SinkProto.ColumnValue.Builder parseValue(JsonNode valueNode, String fiel
case STRING:
case VARBINARY:
{
- String value = valueNode.asText().trim();
+ String value = valueNode.asText();
columnValueBuilder.setValue(ByteString.copyFrom(value, StandardCharsets.UTF_8));
// columnValueBuilder.setType(PixelsProto.Type.newBuilder().setKind(PixelsProto.Type.Kind.STRING));
break;
@@ -175,6 +184,7 @@ private SinkProto.ColumnValue.Builder parseValue(JsonNode valueNode, String fiel
break;
}
case DATE:
+ case TIME:
{
buildInt32(valueNode.asInt(), Integer.BYTES, columnValueBuilder);
// columnValueBuilder.setType(PixelsProto.Type.newBuilder()
@@ -197,168 +207,112 @@ private SinkProto.ColumnValue.Builder parseValue(JsonNode valueNode, String fiel
return columnValueBuilder;
}
- @Deprecated // TODO: use bit
- private SinkProto.ColumnValue.Builder parseValue(GenericRecord record, String fieldName, TypeDescription fieldType)
+ private SinkProto.ColumnValue.Builder parseCanonicalValue(Object raw, TypeDescription type)
{
- SinkProto.ColumnValue.Builder columnValueBuilder = SinkProto.ColumnValue.newBuilder();
- // columnValueBuilder.setName(fieldName);
-
- Object raw = record.get(fieldName);
if (raw == null)
{
- columnValueBuilder.setValue(ByteString.EMPTY);
- return columnValueBuilder;
+ return SinkProto.ColumnValue.newBuilder()
+ .setValue(ByteString.EMPTY);
+ }
+ if (raw instanceof JsonNode jsonNode)
+ {
+ return parseValue(jsonNode, "", type);
}
- switch (fieldType.getCategory())
+ SinkProto.ColumnValue.Builder builder = SinkProto.ColumnValue.newBuilder();
+ switch (type.getCategory())
{
+ case BYTE:
+ buildInt32(((Number) raw).intValue(), Byte.BYTES, builder);
+ break;
+ case SHORT:
+ buildInt32(((Number) raw).intValue(), Short.BYTES, builder);
+ break;
case INT:
- {
- int value = (int) raw;
- columnValueBuilder.setValue(ByteString.copyFrom(Integer.toString(value), StandardCharsets.UTF_8));
- // columnValueBuilder.setType(PixelsProto.Type.newBuilder().setKind(PixelsProto.Type.Kind.INT));
+ buildInt32(((Number) raw).intValue(), Integer.BYTES, builder);
break;
- }
-
case LONG:
+ case TIMESTAMP:
{
- long value = (long) raw;
- columnValueBuilder.setValue(ByteString.copyFrom(Long.toString(value), StandardCharsets.UTF_8));
- // columnValueBuilder.setType(PixelsProto.Type.newBuilder().setKind(PixelsProto.Type.Kind.LONG));
+ byte[] bytes = ByteBuffer.allocate(Long.BYTES)
+ .putLong(((Number) raw).longValue()).array();
+ builder.setValue(ByteString.copyFrom(bytes));
break;
}
-
+ case DATE:
+ case TIME:
+ buildInt32(((Number) raw).intValue(), Integer.BYTES, builder);
+ break;
+ case CHAR:
+ case VARCHAR:
case STRING:
- {
- String value = raw.toString();
- columnValueBuilder.setValue(ByteString.copyFrom(value, StandardCharsets.UTF_8));
- // columnValueBuilder.setType(PixelsProto.Type.newBuilder().setKind(PixelsProto.Type.Kind.STRING));
+ builder.setValue(ByteString.copyFrom(raw.toString(), StandardCharsets.UTF_8));
break;
- }
-
case DECIMAL:
{
- ByteBuffer buffer = (ByteBuffer) raw;
- String decimalStr = new String(buffer.array(), StandardCharsets.UTF_8).trim();
- columnValueBuilder.setValue(ByteString.copyFrom(decimalStr, StandardCharsets.UTF_8));
- // columnValueBuilder.setType(PixelsProto.Type.newBuilder()
-// .setKind(PixelsProto.Type.Kind.DECIMAL)
-// .setDimension(fieldType.getPrecision())
-// .setScale(fieldType.getScale()));
- break;
- }
-
- case DATE:
- {
- int epochDay = (int) raw;
- String isoDate = LocalDate.ofEpochDay(epochDay).toString(); // e.g., "2025-07-03"
- columnValueBuilder.setValue(ByteString.copyFrom(isoDate, StandardCharsets.UTF_8));
- // columnValueBuilder.setType(PixelsProto.Type.newBuilder().setKind(PixelsProto.Type.Kind.DATE));
+ BigDecimal decimal = toBigDecimal(raw, type);
+ builder.setValue(ByteString.copyFrom(
+ decimal.toPlainString(), StandardCharsets.UTF_8));
break;
}
-
case BINARY:
- {
- ByteBuffer buffer = (ByteBuffer) raw;
- // encode as hex or base64 if needed, otherwise just dump as UTF-8 string if it's meant to be readable
- String base64 = Base64.getEncoder().encodeToString(buffer.array());
- columnValueBuilder.setValue(ByteString.copyFrom(base64, StandardCharsets.UTF_8));
- // columnValueBuilder.setType(PixelsProto.Type.newBuilder().setKind(PixelsProto.Type.Kind.BINARY));
- break;
- }
- default:
- throw new IllegalArgumentException("Unsupported type: " + fieldType.getCategory());
- }
-
- return columnValueBuilder;
- }
-
- private SinkProto.ColumnValue.Builder parseValue(Object record, String fieldName, Schema.Type type)
- {
- // TODO(AntiO2) support pixels type
- if (record == null)
- {
- return SinkProto.ColumnValue.newBuilder()
- // .setName(fieldName)
- .setValue(ByteString.EMPTY);
- }
-
- SinkProto.ColumnValue.Builder columnValueBuilder = SinkProto.ColumnValue.newBuilder();
- switch (type)
- {
- case INT8:
- case INT16:
- {
- buildInt32((Short) record, Integer.BYTES, columnValueBuilder);
- // columnValueBuilder.setType(PixelsProto.Type.newBuilder().setKind(PixelsProto.Type.Kind.INT));
- break;
- }
- case INT32:
- {
- buildInt32((Integer) record, Integer.BYTES, columnValueBuilder);
- // columnValueBuilder.setType(PixelsProto.Type.newBuilder().setKind(PixelsProto.Type.Kind.INT));
+ case VARBINARY:
+ builder.setValue(ByteString.copyFrom(toBytes(raw)));
break;
- }
- case INT64:
+ case DOUBLE:
{
- long value = (Long) record;
- byte[] bytes = ByteBuffer.allocate(Long.BYTES).putLong(value).array();
- columnValueBuilder.setValue(ByteString.copyFrom(bytes));
- // columnValueBuilder.setType(PixelsProto.Type.newBuilder().setKind(PixelsProto.Type.Kind.LONG));
+ long bits = Double.doubleToLongBits(((Number) raw).doubleValue());
+ builder.setValue(ByteString.copyFrom(
+ ByteBuffer.allocate(Long.BYTES).putLong(bits).array()));
break;
}
- case BYTES:
- {
- if (record instanceof BigDecimal)
- {
- float value = ((BigDecimal) record).floatValue();
- buildFloat32(value, columnValueBuilder);
- break;
- }
- byte[] bytes = (byte[]) record;
- columnValueBuilder.setValue(ByteString.copyFrom(bytes));
- // columnValueBuilder.setType(PixelsProto.Type.newBuilder().setKind(PixelsProto.Type.Kind.BYTE));
+ case FLOAT:
+ buildFloat32(((Number) raw).floatValue(), builder);
break;
- }
case BOOLEAN:
- case STRING:
- {
- String value = (String) record;
- columnValueBuilder.setValue(ByteString.copyFrom(value, StandardCharsets.UTF_8));
-// columnValueBuilder.setType(PixelsProto.Type.newBuilder().setKind(PixelsProto.Type.Kind.STRING));
+ builder.setValue(ByteString.copyFrom(
+ new byte[]{(byte) ((Boolean) raw ? 1 : 0)}));
break;
- }
case STRUCT:
- {
- // You can recursively parse fields in a struct here
throw new UnsupportedOperationException("STRUCT parsing not yet implemented");
- }
- case FLOAT64:
- {
- double value = (double) record;
- long doubleBits = Double.doubleToLongBits(value);
- byte[] bytes = ByteBuffer.allocate(Long.BYTES).putLong(doubleBits).array();
- columnValueBuilder.setValue(ByteString.copyFrom(bytes));
- // columnValueBuilder.setType(PixelsProto.Type.newBuilder().setKind(PixelsProto.Type.Kind.DOUBLE));
- break;
- }
- case FLOAT32:
- {
- float value = (float) record;
- buildFloat32(value, columnValueBuilder);
- // columnValueBuilder.setType(PixelsProto.Type.newBuilder().setKind(PixelsProto.Type.Kind.FLOAT));
- break;
- }
default:
- throw new IllegalArgumentException("Unsupported type: " + type);
+ throw new IllegalArgumentException(
+ "Unsupported canonical type: " + type.getCategory());
}
+ return builder;
+ }
- return columnValueBuilder;
+ private BigDecimal toBigDecimal(Object raw, TypeDescription type)
+ {
+ if (raw instanceof BigDecimal decimal)
+ {
+ return decimal;
+ }
+ if (raw instanceof ByteBuffer buffer)
+ {
+ return new BigDecimal(new BigInteger(toBytes(buffer)), type.getScale());
+ }
+ if (raw instanceof byte[] bytes)
+ {
+ return new BigDecimal(new BigInteger(bytes), type.getScale());
+ }
+ return new BigDecimal(raw.toString());
}
- private Map parseDeleteRecord()
+ private byte[] toBytes(Object raw)
{
- return Collections.singletonMap("__deleted", true);
+ if (raw instanceof byte[] bytes)
+ {
+ return bytes;
+ }
+ if (raw instanceof ByteBuffer buffer)
+ {
+ ByteBuffer copy = buffer.duplicate();
+ byte[] bytes = new byte[copy.remaining()];
+ copy.get(bytes);
+ return bytes;
+ }
+ return raw.toString().getBytes(StandardCharsets.UTF_8);
}
BigDecimal parseDecimal(JsonNode node, TypeDescription type)
@@ -368,19 +322,4 @@ BigDecimal parseDecimal(JsonNode node, TypeDescription type)
return new BigDecimal(new BigInteger(bytes), scale);
}
- private LocalDate parseDate(JsonNode node)
- {
- return LocalDate.ofEpochDay(node.asLong());
- }
-
- private byte[] parseBinary(JsonNode node)
- {
- try
- {
- return node.binaryValue();
- } catch (IOException e)
- {
- throw new RuntimeException("Binary parsing failed", e);
- }
- }
-}
\ No newline at end of file
+}
diff --git a/src/main/java/io/pixelsdb/pixels/sink/conversion/sinkproto/RowRecordConverter.java b/src/main/java/io/pixelsdb/pixels/sink/conversion/sinkproto/RowRecordConverter.java
new file mode 100644
index 0000000..319c657
--- /dev/null
+++ b/src/main/java/io/pixelsdb/pixels/sink/conversion/sinkproto/RowRecordConverter.java
@@ -0,0 +1,51 @@
+/*
+ * Copyright 2026 PixelsDB.
+ *
+ * Licensed 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 io.pixelsdb.pixels.sink.conversion.sinkproto;
+
+import com.google.protobuf.InvalidProtocolBufferException;
+import io.pixelsdb.pixels.core.TypeDescription;
+import io.pixelsdb.pixels.sink.SinkProto;
+import io.pixelsdb.pixels.sink.event.RowChangeEvent;
+import io.pixelsdb.pixels.sink.event.RowChangeEventFactory;
+import io.pixelsdb.pixels.sink.exception.SinkException;
+import io.pixelsdb.pixels.sink.metadata.TableMetadata;
+import io.pixelsdb.pixels.sink.metadata.TableMetadataRegistry;
+
+import java.nio.ByteBuffer;
+
+public final class RowRecordConverter
+{
+ private final TableMetadataRegistry tableMetadataRegistry;
+
+ public RowRecordConverter(TableMetadataRegistry tableMetadataRegistry)
+ {
+ this.tableMetadataRegistry = tableMetadataRegistry;
+ }
+
+ public RowChangeEvent convert(SinkProto.RowRecord rowRecord) throws SinkException
+ {
+ SinkProto.SourceInfo source = rowRecord.getSource();
+ TableMetadata metadata = tableMetadataRegistry.getMetadata(
+ source.getDb(), source.getTable());
+ TypeDescription schema = metadata.getTypeDescription();
+ return RowChangeEventFactory.create(rowRecord, schema, metadata);
+ }
+
+ public SinkProto.RowRecord parse(ByteBuffer buffer) throws InvalidProtocolBufferException
+ {
+ return SinkProto.RowRecord.parseFrom(buffer.duplicate());
+ }
+}
diff --git a/src/main/java/io/pixelsdb/pixels/sink/conversion/sinkproto/TransactionMetadataConverter.java b/src/main/java/io/pixelsdb/pixels/sink/conversion/sinkproto/TransactionMetadataConverter.java
new file mode 100644
index 0000000..b40a070
--- /dev/null
+++ b/src/main/java/io/pixelsdb/pixels/sink/conversion/sinkproto/TransactionMetadataConverter.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright 2026 PixelsDB.
+ *
+ * Licensed 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 io.pixelsdb.pixels.sink.conversion.sinkproto;
+
+import com.google.protobuf.InvalidProtocolBufferException;
+import io.pixelsdb.pixels.sink.SinkProto;
+
+import java.nio.ByteBuffer;
+
+public final class TransactionMetadataConverter
+{
+ public SinkProto.TransactionMetadata convert(ByteBuffer buffer, int loopId)
+ throws InvalidProtocolBufferException
+ {
+ SinkProto.TransactionMetadata metadata =
+ SinkProto.TransactionMetadata.parseFrom(buffer.duplicate());
+ return metadata.toBuilder()
+ .setId(metadata.getId() + "_" + loopId)
+ .build();
+ }
+}
diff --git a/src/main/java/io/pixelsdb/pixels/sink/event/RowChangeEvent.java b/src/main/java/io/pixelsdb/pixels/sink/event/RowChangeEvent.java
index b3588fd..19336e7 100644
--- a/src/main/java/io/pixelsdb/pixels/sink/event/RowChangeEvent.java
+++ b/src/main/java/io/pixelsdb/pixels/sink/event/RowChangeEvent.java
@@ -1,23 +1,18 @@
/*
* Copyright 2025 PixelsDB.
*
- * This file is part of Pixels.
+ * Licensed 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
*
- * Pixels is free software: you can redistribute it and/or modify
- * it under the terms of the Affero GNU General Public License as
- * published by the Free Software Foundation, either version 3 of
- * the License, or (at your option) any later version.
+ * http://www.apache.org/licenses/LICENSE-2.0
*
- * Pixels is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * Affero GNU General Public License for more details.
- *
- * You should have received a copy of the Affero GNU General Public
- * License along with Pixels. If not, see
- * .
+ * 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 io.pixelsdb.pixels.sink.event;
import com.google.protobuf.ByteString;
@@ -30,8 +25,6 @@
import io.pixelsdb.pixels.sink.exception.SinkException;
import io.pixelsdb.pixels.sink.metadata.TableMetadata;
import io.pixelsdb.pixels.sink.metadata.TableMetadataRegistry;
-import io.pixelsdb.pixels.sink.util.MetricsFacade;
-import io.prometheus.client.Summary;
import lombok.Getter;
import lombok.Setter;
@@ -47,7 +40,6 @@ public class RowChangeEvent
@Getter
private final SinkProto.RowRecord rowRecord;
- private final MetricsFacade metricsFacade = MetricsFacade.getInstance();
@Getter
private final TypeDescription schema;
/**
@@ -60,7 +52,6 @@ public class RowChangeEvent
private String topic;
@Getter
private TableMetadata tableMetadata = null;
- private Summary.Timer latencyTimer;
private Map beforeValueMap;
private Map afterValueMap;
@Getter
@@ -78,20 +69,31 @@ public class RowChangeEvent
public RowChangeEvent(SinkProto.RowRecord rowRecord) throws SinkException
{
- this.rowRecord = rowRecord;
TableMetadataRegistry tableMetadataRegistry = TableMetadataRegistry.Instance();
- this.schema = tableMetadataRegistry.getTypeDescription(getSchemaName(), getTable());
+ this.rowRecord = rowRecord;
+ this.schema = tableMetadataRegistry.getTypeDescription(
+ rowRecord.getSource().getDb(), rowRecord.getSource().getTable());
+ this.tableMetadata = tableMetadataRegistry.getMetadata(
+ rowRecord.getSource().getDb(), rowRecord.getSource().getTable());
init();
initIndexKey();
}
public RowChangeEvent(SinkProto.RowRecord rowRecord, TypeDescription schema) throws SinkException
+ {
+ this(rowRecord, schema, TableMetadataRegistry.Instance().getMetadata(
+ rowRecord.getSource().getDb(), rowRecord.getSource().getTable()));
+ }
+
+ public RowChangeEvent(
+ SinkProto.RowRecord rowRecord,
+ TypeDescription schema,
+ TableMetadata tableMetadata) throws SinkException
{
this.rowRecord = rowRecord;
this.schema = schema;
-
+ this.tableMetadata = tableMetadata;
init();
- // initIndexKey();
}
protected static int getBucketFromIndexKey(IndexProto.IndexKey indexKey)
@@ -106,8 +108,7 @@ protected static int getBucketIdFromByteBuffer(ByteString byteString)
private void init() throws SinkException
{
- TableMetadataRegistry tableMetadataRegistry = TableMetadataRegistry.Instance();
- this.tableId = tableMetadataRegistry.getTableId(getSchemaName(), getTable());
+ this.tableId = tableMetadata == null ? 0 : tableMetadata.getTableId();
this.schemaTableName = new SchemaTableName(getSchemaName(), getTable());
initColumnValueMap();
@@ -141,9 +142,10 @@ public void initIndexKey() throws SinkException
return;
}
- this.tableMetadata = TableMetadataRegistry.Instance().getMetadata(
- this.rowRecord.getSource().getDb(),
- this.rowRecord.getSource().getTable());
+ if (this.tableMetadata == null)
+ {
+ throw new SinkException("Row change table metadata is missing");
+ }
if (!this.tableMetadata.hasPrimaryIndex())
{
@@ -258,16 +260,14 @@ public String getTable()
public String getFullTableName()
{
- // TODO(AntiO2): In postgresql, data collection uses schemaName as prefix, while MySQL uses DB as prefix.
- return rowRecord.getSource().getSchema() + "." + rowRecord.getSource().getTable();
- // return getSchemaName() + "." + getTable();
+ SinkProto.SourceInfo source = rowRecord.getSource();
+ String namespace = source.getSchema().isBlank() ? source.getDb() : source.getSchema();
+ return namespace + "." + source.getTable();
}
- // TODO(AntiO2): How to Map Schema Names Between Source DB and Pixels
public String getSchemaName()
{
return rowRecord.getSource().getDb();
- // return rowRecord.getSource().getSchema();
}
public boolean hasError()
@@ -310,20 +310,6 @@ public boolean hasAfterData()
return isUpdate() || isInsert() || isSnapshot();
}
- public void startLatencyTimer()
- {
- this.latencyTimer = metricsFacade.startProcessLatencyTimer();
- }
-
- public void endLatencyTimer()
- {
- if (latencyTimer != null)
- {
- this.latencyTimer.close();
- }
-
- }
-
public SinkProto.OperationType getOp()
{
return rowRecord.getOp();
diff --git a/src/main/java/io/pixelsdb/pixels/sink/event/RowChangeEventFactory.java b/src/main/java/io/pixelsdb/pixels/sink/event/RowChangeEventFactory.java
new file mode 100644
index 0000000..07eabdc
--- /dev/null
+++ b/src/main/java/io/pixelsdb/pixels/sink/event/RowChangeEventFactory.java
@@ -0,0 +1,101 @@
+/*
+ * Copyright 2026 PixelsDB.
+ *
+ * Licensed 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 io.pixelsdb.pixels.sink.event;
+
+import io.pixelsdb.pixels.core.TypeDescription;
+import io.pixelsdb.pixels.sink.SinkProto;
+import io.pixelsdb.pixels.sink.exception.SinkException;
+import io.pixelsdb.pixels.sink.metadata.TableMetadata;
+import io.pixelsdb.pixels.sink.metadata.TableMetadataRegistry;
+
+public final class RowChangeEventFactory
+{
+ private RowChangeEventFactory()
+ {
+ }
+
+ public static RowChangeEvent create(
+ SinkProto.OperationType operation,
+ SinkProto.SourceInfo source,
+ SinkProto.TransactionInfo transaction,
+ SinkProto.RowValue before,
+ SinkProto.RowValue after,
+ TypeDescription schema) throws SinkException
+ {
+ if (operation == null)
+ {
+ throw new SinkException("Row change operation is missing");
+ }
+ if (source == null)
+ {
+ throw new SinkException("Row change source is missing");
+ }
+
+ SinkProto.RowRecord.Builder builder = SinkProto.RowRecord.newBuilder()
+ .setOp(operation)
+ .setSource(source);
+ if (transaction != null)
+ {
+ builder.setTransaction(transaction);
+ }
+ if (before != null)
+ {
+ builder.setBefore(before);
+ }
+ if (after != null)
+ {
+ builder.setAfter(after);
+ }
+ return create(builder.build(), schema);
+ }
+
+ public static RowChangeEvent create(
+ SinkProto.RowRecord rowRecord,
+ TypeDescription schema) throws SinkException
+ {
+ if (rowRecord == null)
+ {
+ throw new SinkException("Row change record is missing");
+ }
+ if (schema == null)
+ {
+ throw new SinkException("Row change schema is missing");
+ }
+ TableMetadataRegistry registry = TableMetadataRegistry.Instance();
+ TableMetadata metadata = registry.getMetadata(
+ rowRecord.getSource().getDb(), rowRecord.getSource().getTable());
+ return create(rowRecord, schema, metadata);
+ }
+
+ public static RowChangeEvent create(
+ SinkProto.RowRecord rowRecord,
+ TypeDescription schema,
+ TableMetadata metadata) throws SinkException
+ {
+ if (rowRecord == null)
+ {
+ throw new SinkException("Row change record is missing");
+ }
+ if (schema == null)
+ {
+ throw new SinkException("Row change schema is missing");
+ }
+
+ RowChangeEvent event = new RowChangeEvent(rowRecord, schema, metadata);
+ event.initIndexKey();
+ return event;
+ }
+}
diff --git a/src/main/java/io/pixelsdb/pixels/sink/event/deserializer/RowChangeEventAvroDeserializer.java b/src/main/java/io/pixelsdb/pixels/sink/event/deserializer/RowChangeEventAvroDeserializer.java
deleted file mode 100644
index 7a2711a..0000000
--- a/src/main/java/io/pixelsdb/pixels/sink/event/deserializer/RowChangeEventAvroDeserializer.java
+++ /dev/null
@@ -1,169 +0,0 @@
-/*
- * Copyright 2025 PixelsDB.
- *
- * This file is part of Pixels.
- *
- * Pixels is free software: you can redistribute it and/or modify
- * it under the terms of the Affero GNU General Public License as
- * published by the Free Software Foundation, either version 3 of
- * the License, or (at your option) any later version.
- *
- * Pixels is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * Affero GNU General Public License for more details.
- *
- * You should have received a copy of the Affero GNU General Public
- * License along with Pixels. If not, see
- * .
- */
-
-package io.pixelsdb.pixels.sink.event.deserializer;
-
-import io.apicurio.registry.serde.SerdeConfig;
-import io.apicurio.registry.serde.avro.AvroKafkaDeserializer;
-import io.pixelsdb.pixels.core.TypeDescription;
-import io.pixelsdb.pixels.sink.SinkProto;
-import io.pixelsdb.pixels.sink.config.PixelsSinkConfig;
-import io.pixelsdb.pixels.sink.config.factory.PixelsSinkConfigFactory;
-import io.pixelsdb.pixels.sink.event.RowChangeEvent;
-import io.pixelsdb.pixels.sink.exception.SinkException;
-import io.pixelsdb.pixels.sink.metadata.TableMetadataRegistry;
-import io.pixelsdb.pixels.sink.util.MetricsFacade;
-import org.apache.avro.Schema;
-import org.apache.avro.generic.GenericRecord;
-import org.apache.kafka.common.serialization.Deserializer;
-
-import java.util.HashMap;
-import java.util.Map;
-
-public class RowChangeEventAvroDeserializer implements Deserializer
-{
-
- private final AvroKafkaDeserializer avroDeserializer = new AvroKafkaDeserializer<>();
- private final TableMetadataRegistry tableMetadataRegistry = TableMetadataRegistry.Instance();
- private final PixelsSinkConfig config = PixelsSinkConfigFactory.getInstance();
-
- @Override
- public void configure(Map configs, boolean isKey)
- {
- Map enrichedConfig = new HashMap<>(configs);
- enrichedConfig.put(SerdeConfig.REGISTRY_URL, config.getRegistryUrl());
- enrichedConfig.put(SerdeConfig.CHECK_PERIOD_MS, SerdeConfig.CHECK_PERIOD_MS_DEFAULT);
- avroDeserializer.configure(enrichedConfig, isKey);
- }
-
- @Override
- public RowChangeEvent deserialize(String topic, byte[] data)
- {
- try
- {
- MetricsFacade.getInstance().addRawData(data.length);
- GenericRecord avroRecord = avroDeserializer.deserialize(topic, data);
- Schema avroSchema = avroRecord.getSchema();
- RowChangeEvent rowChangeEvent = convertToRowChangeEvent(avroRecord, avroSchema);
- return rowChangeEvent;
- } catch (Exception e)
- {
- e.printStackTrace();
- return null;
- // throw new SerializationException("Avro deserialization failed", e);
- // TODO: 这里有些Row Change Event 不是完整的结构。
- }
- }
-
- private void registerSchema(String topic, Schema avroSchema)
- {
-
- }
-
- private RowChangeEvent convertToRowChangeEvent(GenericRecord avroRecord, Schema schema) throws SinkException
- {
- SinkProto.OperationType op = parseOperationType(avroRecord);
- SinkProto.RowRecord.Builder recordBuilder = SinkProto.RowRecord.newBuilder()
- .setOp(op)
-// .setTsMs(DeserializerUtil.getLongSafely(avroRecord, "ts_ms"));
- ;
- if (avroRecord.get("source") != null)
- {
- //TODO: 这里看下怎么处理,如果没有source信息,其实可以通过topic推出schema和table信息。
- parseSourceInfo((GenericRecord) avroRecord.get("source"), recordBuilder.getSourceBuilder());
- }
-
- String sourceSchema = recordBuilder.getSource().getDb();
- String sourceTable = recordBuilder.getSource().getTable();
- TypeDescription typeDescription = null;
- try
- {
- typeDescription = tableMetadataRegistry.getTypeDescription(sourceSchema, sourceTable);
- } catch (SinkException e)
- {
- throw new RuntimeException(e);
- }
- // TableMetadata tableMetadata = tableMetadataRegistry.loadTableMetadata(sourceSchema, sourceTable);
-
- recordBuilder.setBefore(parseRowData(avroRecord.get("before"), typeDescription));
- recordBuilder.setAfter(parseRowData(avroRecord.get("after"), typeDescription));
-
- if (avroRecord.get("transaction") != null)
- {
- parseTransactionInfo((GenericRecord) avroRecord.get("transaction"),
- recordBuilder.getTransactionBuilder());
- }
-
- return new RowChangeEvent(recordBuilder.build(), typeDescription);
- }
-
- private SinkProto.OperationType parseOperationType(GenericRecord record)
- {
- String op = DeserializerUtil.getStringSafely(record, "op");
- try
- {
- return DeserializerUtil.getOperationType(op);
- } catch (IllegalArgumentException e)
- {
- return SinkProto.OperationType.UNRECOGNIZED;
- }
- }
-
- private SinkProto.RowValue.Builder parseRowData(Object data, TypeDescription typeDescription)
- {
- SinkProto.RowValue.Builder builder = SinkProto.RowValue.newBuilder();
- if (data instanceof GenericRecord rowData)
- {
- RowDataParser rowDataParser = new RowDataParser(typeDescription); // TODO make it static?
- rowDataParser.parse(rowData, builder);
- }
- return builder;
- }
-
- private void parseSourceInfo(GenericRecord source, SinkProto.SourceInfo.Builder builder)
- {
-
- builder
- .setDb(DeserializerUtil.getStringSafely(source, "db"))
- .setSchema(DeserializerUtil.getStringSafely(source, "schema"))
- .setTable(DeserializerUtil.getStringSafely(source, "table"))
-// .setVersion(DeserializerUtil.getStringSafely(source, "version"))
-// .setConnector(DeserializerUtil.getStringSafely(source, "connector"))
-// .setName(DeserializerUtil.getStringSafely(source, "name"))
-// .setTsMs(DeserializerUtil.getLongSafely(source, "ts_ms"))
-// .setSnapshot(DeserializerUtil.getStringSafely(source, "snapshot"))
-//
-// .setSequence(DeserializerUtil.getStringSafely(source, "sequence"))
-
-// .setTxId(DeserializerUtil.getLongSafely(source, "tx_id"))
-// .setLsn(DeserializerUtil.getLongSafely(source, "lsn"))
-// .setXmin(DeserializerUtil.getLongSafely(source, "xmin"))
- ;
- }
-
- private void parseTransactionInfo(GenericRecord transaction,
- SinkProto.TransactionInfo.Builder builder)
- {
- builder.setId(DeserializerUtil.getTransIdPrefix(DeserializerUtil.getStringSafely(transaction, "id")))
- .setTotalOrder(DeserializerUtil.getLongSafely(transaction, "total_order"))
- .setDataCollectionOrder(DeserializerUtil.getLongSafely(transaction, "data_collection_order"));
- }
-
-}
\ No newline at end of file
diff --git a/src/main/java/io/pixelsdb/pixels/sink/event/deserializer/RowChangeEventJsonDeserializer.java b/src/main/java/io/pixelsdb/pixels/sink/event/deserializer/RowChangeEventJsonDeserializer.java
deleted file mode 100644
index 6d55297..0000000
--- a/src/main/java/io/pixelsdb/pixels/sink/event/deserializer/RowChangeEventJsonDeserializer.java
+++ /dev/null
@@ -1,173 +0,0 @@
-/*
- * Copyright 2025 PixelsDB.
- *
- * This file is part of Pixels.
- *
- * Pixels is free software: you can redistribute it and/or modify
- * it under the terms of the Affero GNU General Public License as
- * published by the Free Software Foundation, either version 3 of
- * the License, or (at your option) any later version.
- *
- * Pixels is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * Affero GNU General Public License for more details.
- *
- * You should have received a copy of the Affero GNU General Public
- * License along with Pixels. If not, see
- * .
- */
-
-package io.pixelsdb.pixels.sink.event.deserializer;
-
-import com.fasterxml.jackson.databind.JsonNode;
-import com.fasterxml.jackson.databind.ObjectMapper;
-import io.pixelsdb.pixels.core.TypeDescription;
-import io.pixelsdb.pixels.sink.SinkProto;
-import io.pixelsdb.pixels.sink.event.RowChangeEvent;
-import io.pixelsdb.pixels.sink.exception.SinkException;
-import io.pixelsdb.pixels.sink.metadata.TableMetadataRegistry;
-import io.pixelsdb.pixels.sink.util.MetricsFacade;
-import org.apache.kafka.common.serialization.Deserializer;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-
-public class RowChangeEventJsonDeserializer implements Deserializer
-{
- private static final Logger logger = LoggerFactory.getLogger(RowChangeEventJsonDeserializer.class);
- private static final ObjectMapper objectMapper = new ObjectMapper();
- private final TableMetadataRegistry tableMetadataRegistry = TableMetadataRegistry.Instance();
-
- @Override
- public RowChangeEvent deserialize(String topic, byte[] data)
- {
- if (data == null || data.length == 0)
- {
- logger.debug("Received empty message from topic: {}", topic);
- return null;
- }
- MetricsFacade.getInstance().addRawData(data.length);
- try
- {
- JsonNode rootNode = objectMapper.readTree(data);
- JsonNode payloadNode = rootNode.path("payload");
-
- SinkProto.OperationType opType = parseOperationType(payloadNode);
-
- return buildRowRecord(payloadNode, opType);
- } catch (Exception e)
- {
- logger.error("Failed to deserialize message from topic {}: {}", topic, e.getMessage());
- return null;
- }
- }
-
- private SinkProto.OperationType parseOperationType(JsonNode payloadNode)
- {
- String opCode = payloadNode.path("op").asText("");
- return DeserializerUtil.getOperationType(opCode);
- }
-
- @Deprecated
- private TypeDescription getSchema(JsonNode schemaNode, SinkProto.OperationType opType)
- {
- return switch (opType)
- {
- case DELETE -> SchemaDeserializer.parseFromBeforeOrAfter(schemaNode, "before");
- case INSERT, UPDATE, SNAPSHOT -> SchemaDeserializer.parseFromBeforeOrAfter(schemaNode, "after");
- case UNRECOGNIZED -> throw new IllegalArgumentException("Operation type is unknown. Check op");
- };
- }
-
- private RowChangeEvent buildRowRecord(JsonNode payloadNode,
- SinkProto.OperationType opType) throws SinkException
- {
-
- SinkProto.RowRecord.Builder builder = SinkProto.RowRecord.newBuilder();
-
- builder.setOp(parseOperationType(payloadNode));
-// .setTsMs(payloadNode.path("ts_ms").asLong())
-// .setTsUs(payloadNode.path("ts_us").asLong())
-// .setTsNs(payloadNode.path("ts_ns").asLong());
-
- String schemaName;
- String tableName;
- if (payloadNode.has("source"))
- {
- SinkProto.SourceInfo.Builder sourceInfoBuilder = parseSourceInfo(payloadNode.get("source"));
- schemaName = sourceInfoBuilder.getDb(); // Notice we use the schema
- tableName = sourceInfoBuilder.getTable();
- builder.setSource(sourceInfoBuilder);
- } else
- {
- throw new IllegalArgumentException("Missing source field in row record");
- }
-
- TypeDescription typeDescription = tableMetadataRegistry.getTypeDescription(schemaName, tableName);
- RowDataParser rowDataParser = new RowDataParser(typeDescription);
- if (payloadNode.hasNonNull("transaction"))
- {
- builder.setTransaction(parseTransactionInfo(payloadNode.get("transaction")));
- }
-
- if (DeserializerUtil.hasBeforeValue(opType))
- {
- SinkProto.RowValue.Builder beforeBuilder = builder.getBeforeBuilder();
- rowDataParser.parse(payloadNode.get("before"), beforeBuilder);
- builder.setBefore(beforeBuilder);
- }
-
- if (DeserializerUtil.hasAfterValue(opType))
- {
-
- SinkProto.RowValue.Builder afterBuilder = builder.getAfterBuilder();
- rowDataParser.parse(payloadNode.get("after"), afterBuilder);
- builder.setAfter(afterBuilder);
- }
-
- RowChangeEvent event = new RowChangeEvent(builder.build(), typeDescription);
- try
- {
- event.initIndexKey();
- } catch (SinkException e)
- {
- logger.warn("Row change event {}: Init index key failed", event);
- }
-
- return event;
- }
-
- private SinkProto.SourceInfo.Builder parseSourceInfo(JsonNode sourceNode)
- {
- return SinkProto.SourceInfo.newBuilder()
- .setDb(sourceNode.path("db").asText())
- .setSchema(sourceNode.path("schema").asText())
- .setTable(sourceNode.path("table").asText())
-// .setVersion(sourceNode.path("version").asText())
-// .setConnector(sourceNode.path("connector").asText())
-// .setName(sourceNode.path("name").asText())
-// .setTsMs(sourceNode.path("ts_ms").asLong())
-// .setSnapshot(sourceNode.path("snapshot").asText())
-
-// .setSequence(sourceNode.path("sequence").asText())
-// .setTsUs(sourceNode.path("ts_us").asLong())
-// .setTsNs(sourceNode.path("ts_ns").asLong())
-
-// .setTxId(sourceNode.path("txId").asLong())
-// .setLsn(sourceNode.path("lsn").asLong())
-// .setXmin(sourceNode.path("xmin").asLong())
- ;
- }
-
- private SinkProto.TransactionInfo parseTransactionInfo(JsonNode txNode)
- {
- return SinkProto.TransactionInfo.newBuilder()
- .setId(DeserializerUtil.getTransIdPrefix(txNode.path("id").asText()))
- .setTotalOrder(txNode.path("total_order").asLong())
- .setDataCollectionOrder(txNode.path("data_collection_order").asLong())
- .build();
- }
-
-}
-
diff --git a/src/main/java/io/pixelsdb/pixels/sink/event/deserializer/RowChangeEventStructDeserializer.java b/src/main/java/io/pixelsdb/pixels/sink/event/deserializer/RowChangeEventStructDeserializer.java
deleted file mode 100644
index 498d382..0000000
--- a/src/main/java/io/pixelsdb/pixels/sink/event/deserializer/RowChangeEventStructDeserializer.java
+++ /dev/null
@@ -1,145 +0,0 @@
-/*
- * Copyright 2025 PixelsDB.
- *
- * This file is part of Pixels.
- *
- * Pixels is free software: you can redistribute it and/or modify
- * it under the terms of the Affero GNU General Public License as
- * published by the Free Software Foundation, either version 3 of
- * the License, or (at your option) any later version.
- *
- * Pixels is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * Affero GNU General Public License for more details.
- *
- * You should have received a copy of the Affero GNU General Public
- * License along with Pixels. If not, see
- * .
- */
-
-package io.pixelsdb.pixels.sink.event.deserializer;
-
-
-import io.pixelsdb.pixels.core.TypeDescription;
-import io.pixelsdb.pixels.sink.SinkProto;
-import io.pixelsdb.pixels.sink.event.RowChangeEvent;
-import io.pixelsdb.pixels.sink.exception.SinkException;
-import io.pixelsdb.pixels.sink.metadata.TableMetadataRegistry;
-import org.apache.kafka.connect.data.Struct;
-import org.apache.kafka.connect.errors.DataException;
-import org.apache.kafka.connect.source.SourceRecord;
-
-import java.util.logging.Logger;
-
-/**
- * @package: io.pixelsdb.pixels.sink.event.deserializer
- * @className: RowChangeEventStructDeserializer
- * @author: AntiO2
- * @date: 2025/9/26 12:00
- */
-public class RowChangeEventStructDeserializer
-{
- private static final Logger LOGGER = Logger.getLogger(RowChangeEventStructDeserializer.class.getName());
- private static final TableMetadataRegistry tableMetadataRegistry = TableMetadataRegistry.Instance();
-
- public static RowChangeEvent convertToRowChangeEvent(SourceRecord sourceRecord) throws SinkException
- {
- Struct value = (Struct) sourceRecord.value();
- String op = value.getString("op");
- SinkProto.OperationType operationType = DeserializerUtil.getOperationType(op);
- return buildRowRecord(value, operationType);
- }
-
- public static RowChangeEvent convertToRowChangeEvent(SinkProto.RowRecord rowRecord) throws SinkException
- {
- String schemaName = rowRecord.getSource().getDb();
- String tableName = rowRecord.getSource().getTable();
- TypeDescription typeDescription = tableMetadataRegistry.getTypeDescription(schemaName, tableName);
- return new RowChangeEvent(rowRecord, typeDescription);
- }
-
- private static RowChangeEvent buildRowRecord(Struct value,
- SinkProto.OperationType opType) throws SinkException
- {
-
- SinkProto.RowRecord.Builder builder = SinkProto.RowRecord.newBuilder();
-
- builder.setOp(opType);
-
- String schemaName;
- String tableName;
- try
- {
- Struct source = value.getStruct("source");
- SinkProto.SourceInfo.Builder sourceInfoBuilder = parseSourceInfo(source);
- schemaName = sourceInfoBuilder.getDb(); // Notice we use the schema
- tableName = sourceInfoBuilder.getTable();
- builder.setSource(sourceInfoBuilder);
- } catch (DataException e)
- {
- LOGGER.warning("Missing source field in row record");
- throw new SinkException(e);
- }
-
- TypeDescription typeDescription = tableMetadataRegistry.getTypeDescription(schemaName, tableName);
- RowDataParser rowDataParser = new RowDataParser(typeDescription);
-
- try
- {
- Struct transaction = value.getStruct("transaction");
- SinkProto.TransactionInfo transactionInfo = parseTransactionInfo(transaction);
- builder.setTransaction(transactionInfo);
- } catch (DataException e)
- {
- LOGGER.warning("Missing transaction field in row record");
- }
-
- if (DeserializerUtil.hasBeforeValue(opType))
- {
- SinkProto.RowValue.Builder beforeBuilder = builder.getBeforeBuilder();
- rowDataParser.parse(value.getStruct("before"), beforeBuilder);
- builder.setBefore(beforeBuilder);
- }
-
- if (DeserializerUtil.hasAfterValue(opType))
- {
-
- SinkProto.RowValue.Builder afterBuilder = builder.getAfterBuilder();
- rowDataParser.parse(value.getStruct("after"), afterBuilder);
- builder.setAfter(afterBuilder);
- }
-
- RowChangeEvent event = new RowChangeEvent(builder.build(), typeDescription);
- return event;
- }
-
- private static SinkProto.SourceInfo.Builder parseSourceInfo(T source)
- {
- return SinkProto.SourceInfo.newBuilder()
- // .setVersion(DeserializerUtil.getStringSafely(source, "version"))
- // .setConnector(DeserializerUtil.getStringSafely(source, "connector"))
-// .setName(DeserializerUtil.getStringSafely(source, "name"))
-// .setTsMs(DeserializerUtil.getLongSafely(source, "ts_ms"))
-// .setSnapshot(DeserializerUtil.getStringSafely(source, "snapshot"))
- .setDb(DeserializerUtil.getStringSafely(source, "db"))
-// .setSequence(DeserializerUtil.getStringSafely(source, "sequence"))
-// .setTsUs(DeserializerUtil.getLongSafely(source, "ts_us"))
-// .setTsNs(DeserializerUtil.getLongSafely(source, "ts_ns"))
- .setSchema(DeserializerUtil.getStringSafely(source, "schema"))
- .setTable(DeserializerUtil.getStringSafely(source, "table"));
-// .setTxId(DeserializerUtil.getLongSafely(source, "txId"))
-// .setLsn(DeserializerUtil.getLongSafely(source, "lsn"))
-// .setXmin(DeserializerUtil.getLongSafely(source, "xmin"));
- }
-
- private static SinkProto.TransactionInfo parseTransactionInfo(T txNode)
- {
- return SinkProto.TransactionInfo.newBuilder()
- .setId(DeserializerUtil.getTransIdPrefix(
- DeserializerUtil.getStringSafely(txNode, "id")))
- .setTotalOrder(DeserializerUtil.getLongSafely(txNode, "total_order"))
- .setDataCollectionOrder(DeserializerUtil.getLongSafely(txNode, "data_collection_order"))
- .build();
- }
-}
diff --git a/src/main/java/io/pixelsdb/pixels/sink/event/deserializer/SchemaDeserializer.java b/src/main/java/io/pixelsdb/pixels/sink/event/deserializer/SchemaDeserializer.java
deleted file mode 100644
index d33f3fe..0000000
--- a/src/main/java/io/pixelsdb/pixels/sink/event/deserializer/SchemaDeserializer.java
+++ /dev/null
@@ -1,231 +0,0 @@
-/*
- * Copyright 2025 PixelsDB.
- *
- * This file is part of Pixels.
- *
- * Pixels is free software: you can redistribute it and/or modify
- * it under the terms of the Affero GNU General Public License as
- * published by the Free Software Foundation, either version 3 of
- * the License, or (at your option) any later version.
- *
- * Pixels is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * Affero GNU General Public License for more details.
- *
- * You should have received a copy of the Affero GNU General Public
- * License along with Pixels. If not, see
- * .
- */
-
-package io.pixelsdb.pixels.sink.event.deserializer;
-
-import com.fasterxml.jackson.databind.JsonNode;
-import io.pixelsdb.pixels.core.TypeDescription;
-import org.apache.avro.Schema;
-
-import java.util.HashMap;
-import java.util.Iterator;
-import java.util.Map;
-
-public class SchemaDeserializer
-{
- public static TypeDescription parseFromBeforeOrAfter(JsonNode schemaNode, String fieldName)
- {
- JsonNode beforeAfterSchema = findSchemaField(schemaNode, fieldName);
- if (beforeAfterSchema == null)
- {
- throw new IllegalArgumentException("Field '" + fieldName + "' not found in schema");
- }
- return parseStruct(beforeAfterSchema.get("fields"));
- }
-
- private static JsonNode findSchemaField(JsonNode schemaNode, String targetField)
- {
- Iterator fields = schemaNode.get("fields").elements();
- while (fields.hasNext())
- {
- JsonNode field = fields.next();
- if (targetField.equals(field.get("field").asText()))
- {
- return field;
- }
- }
- return null;
- }
-
- public static TypeDescription parseStruct(JsonNode fields)
- {
- TypeDescription structType = TypeDescription.createStruct();
- fields.forEach(field ->
- {
- String name = field.get("field").asText();
- TypeDescription fieldType = parseFieldType(field);
- structType.addField(name, fieldType);
- });
- return structType;
- }
-
- static TypeDescription parseFieldType(JsonNode fieldNode)
- {
- if (!fieldNode.has("type"))
- {
- throw new IllegalArgumentException("Field is missing required 'type' property");
- }
- String typeName = fieldNode.get("type").asText();
- String logicalType = fieldNode.has("name") ? fieldNode.get("name").asText() : null;
-
- if (logicalType != null)
- {
- switch (logicalType)
- {
- case "org.apache.kafka.connect.data.Decimal":
- int precision = Integer.parseInt(fieldNode.get("parameters").get("connect.decimal.precision").asText());
- int scale = Integer.parseInt(fieldNode.get("parameters").get("scale").asText());
- return TypeDescription.createDecimal(precision, scale);
- case "io.debezium.time.Date":
- return TypeDescription.createDate();
- }
- }
-
- switch (typeName)
- {
- case "int64":
- return TypeDescription.createLong();
- case "int32":
- return TypeDescription.createInt();
- case "string":
- return TypeDescription.createString();
- case "struct":
- return parseStruct(fieldNode.get("fields"));
- default:
- throw new IllegalArgumentException("Unsupported type: " + typeName);
- }
- }
-
- public static TypeDescription parseFromBeforeOrAfter(Schema schemaNode, String fieldName)
- {
- Schema.Field filed = schemaNode.getField(fieldName);
- if (filed == null)
- {
- throw new IllegalArgumentException("Can't find field in avro schema: " + fieldName);
- }
-
- Schema valueSchema = filed.schema();
- return parseFromAvroSchema(valueSchema);
- }
-
-
- public static TypeDescription parseFromAvroSchema(Schema avroSchema)
- {
- return parseAvroType(avroSchema, new HashMap<>());
- }
-
- private static TypeDescription parseAvroType(Schema schema, Map cache)
- {
- String schemaKey = schema.getFullName() + ":" + schema.hashCode();
- if (cache.containsKey(schemaKey))
- {
- return cache.get(schemaKey);
- }
-
- TypeDescription typeDesc;
- switch (schema.getType())
- {
- case RECORD:
- typeDesc = parseAvroRecord(schema, cache);
- break;
- case UNION:
- typeDesc = parseAvroUnion(schema, cache);
- break;
- case ARRAY:
- typeDesc = parseAvroArray(schema, cache);
- break;
- case MAP:
- typeDesc = parseAvroMap(schema, cache);
- break;
- default:
- typeDesc = parseAvroPrimitive(schema);
- }
-
- cache.put(schemaKey, typeDesc);
- return typeDesc;
- }
-
- private static TypeDescription parseAvroRecord(Schema schema, Map cache)
- {
- TypeDescription structType = TypeDescription.createStruct();
- for (Schema.Field field : schema.getFields())
- {
- TypeDescription fieldType = parseAvroType(field.schema(), cache);
- structType.addField(field.name(), fieldType);
- }
- return structType;
- }
-
- private static TypeDescription parseAvroUnion(Schema schema, Map cache)
- {
- for (Schema type : schema.getTypes())
- {
- if (type.getType() != Schema.Type.NULL)
- {
- return parseAvroType(type, cache);
- }
- }
- throw new IllegalArgumentException("Invalid union type: " + schema);
- }
-
- private static TypeDescription parseAvroArray(Schema schema, Map cache)
- {
- throw new RuntimeException("Doesn't support Array");
- }
-
- private static TypeDescription parseAvroMap(Schema schema, Map cache)
- {
- throw new RuntimeException("Doesn't support Map");
- }
-
- private static TypeDescription parseAvroPrimitive(Schema schema)
- {
- String logicalType = schema.getLogicalType() != null ?
- schema.getLogicalType().getName() : null;
-
- if (logicalType != null)
- {
- switch (logicalType)
- {
- case "decimal":
- return TypeDescription.createDecimal(
- (Integer) (schema.getObjectProp("precision")),
- (Integer) (schema.getObjectProp("scale"))
- );
- case "date":
- return TypeDescription.createDate();
- case "timestamp-millis":
- return TypeDescription.createTimestamp((Integer) (schema.getObjectProp("precision")));
- case "uuid":
- return TypeDescription.createString();
- }
- }
-
- switch (schema.getType())
- {
- case LONG:
- return TypeDescription.createLong();
- case INT:
- return TypeDescription.createInt();
- case STRING:
- return TypeDescription.createString();
- case BOOLEAN:
- return TypeDescription.createBoolean();
- case FLOAT:
- return TypeDescription.createFloat();
- case DOUBLE:
- return TypeDescription.createDouble();
- case BYTES:
- // return TypeDescription.createBinary();
- default:
- throw new IllegalArgumentException("Unsupported Avro type: " + schema);
- }
- }
-}
diff --git a/src/main/java/io/pixelsdb/pixels/sink/event/deserializer/TransactionAvroMessageDeserializer.java b/src/main/java/io/pixelsdb/pixels/sink/event/deserializer/TransactionAvroMessageDeserializer.java
deleted file mode 100644
index ffdf3f6..0000000
--- a/src/main/java/io/pixelsdb/pixels/sink/event/deserializer/TransactionAvroMessageDeserializer.java
+++ /dev/null
@@ -1,77 +0,0 @@
-/*
- * Copyright 2025 PixelsDB.
- *
- * This file is part of Pixels.
- *
- * Pixels is free software: you can redistribute it and/or modify
- * it under the terms of the Affero GNU General Public License as
- * published by the Free Software Foundation, either version 3 of
- * the License, or (at your option) any later version.
- *
- * Pixels is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * Affero GNU General Public License for more details.
- *
- * You should have received a copy of the Affero GNU General Public
- * License along with Pixels. If not, see
- * .
- */
-
-package io.pixelsdb.pixels.sink.event.deserializer;
-
-import io.apicurio.registry.serde.SerdeConfig;
-import io.apicurio.registry.serde.avro.AvroKafkaDeserializer;
-import io.pixelsdb.pixels.sink.SinkProto;
-import io.pixelsdb.pixels.sink.config.PixelsSinkConfig;
-import io.pixelsdb.pixels.sink.config.factory.PixelsSinkConfigFactory;
-import io.pixelsdb.pixels.sink.util.MetricsFacade;
-import org.apache.avro.generic.GenericRecord;
-import org.apache.kafka.common.errors.SerializationException;
-import org.apache.kafka.common.serialization.Deserializer;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import java.util.HashMap;
-import java.util.Map;
-
-public class TransactionAvroMessageDeserializer implements Deserializer
-{
- private static final Logger logger = LoggerFactory.getLogger(TransactionAvroMessageDeserializer.class);
- private final AvroKafkaDeserializer avroDeserializer = new AvroKafkaDeserializer<>();
- private final PixelsSinkConfig config = PixelsSinkConfigFactory.getInstance();
-
- @Override
- public void configure(Map configs, boolean isKey)
- {
- Map enrichedConfig = new HashMap<>(configs);
- enrichedConfig.put(SerdeConfig.REGISTRY_URL, config.getRegistryUrl());
- enrichedConfig.put(SerdeConfig.CHECK_PERIOD_MS, SerdeConfig.CHECK_PERIOD_MS_DEFAULT);
- avroDeserializer.configure(enrichedConfig, isKey);
- }
-
- @Override
- public SinkProto.TransactionMetadata deserialize(String topic, byte[] bytes)
- {
- if (bytes == null || bytes.length == 0)
- {
- return null;
- }
- try
- {
- MetricsFacade.getInstance().addRawData(bytes.length);
- GenericRecord avroRecord = avroDeserializer.deserialize(topic, bytes);
- return TransactionStructMessageDeserializer.convertToTransactionMetadata(avroRecord);
- } catch (Exception e)
- {
- logger.error("Avro deserialization failed for topic {}: {}", topic, e.getMessage());
- throw new SerializationException("Failed to deserialize Avro message", e);
- }
- }
-
- @Override
- public void close()
- {
- Deserializer.super.close();
- }
-}
diff --git a/src/main/java/io/pixelsdb/pixels/sink/event/deserializer/TransactionJsonMessageDeserializer.java b/src/main/java/io/pixelsdb/pixels/sink/event/deserializer/TransactionJsonMessageDeserializer.java
deleted file mode 100644
index c09ba3e..0000000
--- a/src/main/java/io/pixelsdb/pixels/sink/event/deserializer/TransactionJsonMessageDeserializer.java
+++ /dev/null
@@ -1,70 +0,0 @@
-/*
- * Copyright 2025 PixelsDB.
- *
- * This file is part of Pixels.
- *
- * Pixels is free software: you can redistribute it and/or modify
- * it under the terms of the Affero GNU General Public License as
- * published by the Free Software Foundation, either version 3 of
- * the License, or (at your option) any later version.
- *
- * Pixels is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * Affero GNU General Public License for more details.
- *
- * You should have received a copy of the Affero GNU General Public
- * License along with Pixels. If not, see
- * .
- */
-
-package io.pixelsdb.pixels.sink.event.deserializer;
-
-import com.fasterxml.jackson.databind.ObjectMapper;
-import com.google.protobuf.util.JsonFormat;
-import io.pixelsdb.pixels.sink.SinkProto;
-import io.pixelsdb.pixels.sink.util.MetricsFacade;
-import org.apache.kafka.common.serialization.Deserializer;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import java.io.IOException;
-import java.util.Map;
-
-public class TransactionJsonMessageDeserializer implements Deserializer
-{
-
- private static final Logger LOGGER = LoggerFactory.getLogger(TransactionJsonMessageDeserializer.class);
- private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
- private static final JsonFormat.Parser PROTO_PARSER = JsonFormat.parser().ignoringUnknownFields();
-
- @Override
- public SinkProto.TransactionMetadata deserialize(String topic, byte[] data)
- {
- if (data == null || data.length == 0)
- {
- return null;
- }
- MetricsFacade.getInstance().addRawData(data.length);
- try
- {
- Map rawMessage = OBJECT_MAPPER.readValue(data, Map.class);
- return parseTransactionMetadata(rawMessage);
- } catch (IOException e)
- {
- LOGGER.error("Failed to deserialize transaction message", e);
- throw new RuntimeException("Deserialization error", e);
- }
- }
-
- private SinkProto.TransactionMetadata parseTransactionMetadata(Map rawMessage) throws IOException
- {
- SinkProto.TransactionMetadata.Builder builder = SinkProto.TransactionMetadata.newBuilder();
- String json = OBJECT_MAPPER.writeValueAsString(rawMessage.get("payload"));
- PROTO_PARSER.merge(json, builder);
-
- builder.setId(DeserializerUtil.getTransIdPrefix(builder.getId()));
-
- return builder.build();
- }
-}
\ No newline at end of file
diff --git a/src/main/java/io/pixelsdb/pixels/sink/event/deserializer/TransactionStructMessageDeserializer.java b/src/main/java/io/pixelsdb/pixels/sink/event/deserializer/TransactionStructMessageDeserializer.java
deleted file mode 100644
index dd521cb..0000000
--- a/src/main/java/io/pixelsdb/pixels/sink/event/deserializer/TransactionStructMessageDeserializer.java
+++ /dev/null
@@ -1,78 +0,0 @@
-/*
- * Copyright 2025 PixelsDB.
- *
- * This file is part of Pixels.
- *
- * Pixels is free software: you can redistribute it and/or modify
- * it under the terms of the Affero GNU General Public License as
- * published by the Free Software Foundation, either version 3 of
- * the License, or (at your option) any later version.
- *
- * Pixels is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * Affero GNU General Public License for more details.
- *
- * You should have received a copy of the Affero GNU General Public
- * License along with Pixels. If not, see
- * .
- */
-
-package io.pixelsdb.pixels.sink.event.deserializer;
-
-
-import io.pixelsdb.pixels.sink.SinkProto;
-import org.apache.avro.generic.GenericRecord;
-import org.apache.kafka.connect.data.Struct;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-/**
- * @package: io.pixelsdb.pixels.sink.event.deserializer
- * @className: TransactionStructMessageDeserializer
- * @author: AntiO2
- * @date: 2025/9/26 12:42
- */
-public class TransactionStructMessageDeserializer
-{
- private static final Logger LOGGER = LoggerFactory.getLogger(TransactionStructMessageDeserializer.class);
-
- @SuppressWarnings("unchecked")
- public static SinkProto.TransactionMetadata convertToTransactionMetadata(T record)
- {
- SinkProto.TransactionMetadata.Builder builder = SinkProto.TransactionMetadata.newBuilder();
-
- builder.setStatus(DeserializerUtil.getStatusSafely(record, "status"))
- .setId(DeserializerUtil.getTransIdPrefix(
- DeserializerUtil.getStringSafely(record, "id")))
- .setEventCount(DeserializerUtil.getLongSafely(record, "event_count"))
- .setTimestamp(DeserializerUtil.getLongSafely(record, "ts_ms"));
-
- Object collections = DeserializerUtil.getFieldSafely(record, "data_collections");
- if (collections instanceof Iterable>)
- {
- for (Object item : (Iterable>) collections)
- {
- if (item instanceof GenericRecord collectionRecord)
- {
- SinkProto.DataCollection.Builder collectionBuilder = SinkProto.DataCollection.newBuilder();
- collectionBuilder.setDataCollection(
- DeserializerUtil.getStringSafely(collectionRecord, "data_collection"));
- collectionBuilder.setEventCount(
- DeserializerUtil.getLongSafely(collectionRecord, "event_count"));
- builder.addDataCollections(collectionBuilder);
- } else if (item instanceof Struct collectionRecord)
- {
- SinkProto.DataCollection.Builder collectionBuilder = SinkProto.DataCollection.newBuilder();
- collectionBuilder.setDataCollection(
- DeserializerUtil.getStringSafely(collectionRecord, "data_collection"));
- collectionBuilder.setEventCount(
- DeserializerUtil.getLongSafely(collectionRecord, "event_count"));
- builder.addDataCollections(collectionBuilder);
- }
- }
- }
-
- return builder.build();
- }
-}
diff --git a/src/main/java/io/pixelsdb/pixels/sink/exception/SinkException.java b/src/main/java/io/pixelsdb/pixels/sink/exception/SinkException.java
index 5ad1c7a..ce208be 100644
--- a/src/main/java/io/pixelsdb/pixels/sink/exception/SinkException.java
+++ b/src/main/java/io/pixelsdb/pixels/sink/exception/SinkException.java
@@ -1,23 +1,18 @@
/*
* Copyright 2025 PixelsDB.
*
- * This file is part of Pixels.
+ * Licensed 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
*
- * Pixels is free software: you can redistribute it and/or modify
- * it under the terms of the Affero GNU General Public License as
- * published by the Free Software Foundation, either version 3 of
- * the License, or (at your option) any later version.
+ * http://www.apache.org/licenses/LICENSE-2.0
*
- * Pixels is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * Affero GNU General Public License for more details.
- *
- * You should have received a copy of the Affero GNU General Public
- * License along with Pixels. If not, see
- * .
+ * 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 io.pixelsdb.pixels.sink.exception;
public class SinkException extends Exception
diff --git a/src/main/java/io/pixelsdb/pixels/sink/freshness/FreshnessClient.java b/src/main/java/io/pixelsdb/pixels/sink/freshness/FreshnessClient.java
index da35c54..b94ad43 100644
--- a/src/main/java/io/pixelsdb/pixels/sink/freshness/FreshnessClient.java
+++ b/src/main/java/io/pixelsdb/pixels/sink/freshness/FreshnessClient.java
@@ -1,23 +1,18 @@
/*
* Copyright 2025 PixelsDB.
*
- * This file is part of Pixels.
+ * Licensed 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
*
- * Pixels is free software: you can redistribute it and/or modify
- * it under the terms of the Affero GNU General Public License as
- * published by the Free Software Foundation, either version 3 of
- * the License, or (at your option) any later version.
+ * http://www.apache.org/licenses/LICENSE-2.0
*
- * Pixels is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * Affero GNU General Public License for more details.
- *
- * You should have received a copy of the Affero GNU General Public
- * License along with Pixels. If not, see
- * .
+ * 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 io.pixelsdb.pixels.sink.freshness;
import io.pixelsdb.pixels.common.exception.TransException;
@@ -36,8 +31,8 @@
import java.util.concurrent.*;
/**
- * FreshnessClient is responsible for monitoring data freshness by periodically
- * querying the maximum timestamp from a set of dynamically configured tables via Trino JDBC.
+ * FreshnessClient monitors data freshness by periodically querying max(freshness_ts)
+ * via JDBC (Trino by default, or HiveServer2 for Hudi when built with {@code -Phudi-hive}).
*/
public class FreshnessClient
{
@@ -45,9 +40,10 @@ public class FreshnessClient
private static final int QUERY_INTERVAL_SECONDS = 1;
private static volatile FreshnessClient instance;
// Configuration parameters (should ideally be loaded from a config file)
- private final String trinoJdbcUrl;
- private final String trinoUser;
- private final String trinoPassword;
+ private final String queryJdbcUrl;
+ private final String queryUser;
+ private final String queryPassword;
+ private final QueryEngine queryEngine;
private final int maxConcurrentQueries;
private final Semaphore queryPermits;
private final ThreadPoolExecutor connectionExecutor;
@@ -58,17 +54,35 @@ public class FreshnessClient
private final int warmUpSeconds;
private final PixelsSinkConfig config;
+ private enum QueryEngine
+ {
+ TRINO("io.trino.jdbc.TrinoDriver",
+ "Trino JDBC driver not found on classpath."),
+ HIVE("org.apache.hive.jdbc.HiveDriver",
+ "Hive JDBC driver not found. Build with -Phudi-hive to enable Hudi freshness via HiveServer2.");
+
+ private final String driverClass;
+ private final String missingHint;
+
+ QueryEngine(String driverClass, String missingHint)
+ {
+ this.driverClass = driverClass;
+ this.missingHint = missingHint;
+ }
+ }
+
private FreshnessClient()
{
// Initializes the set with thread safety wrapper
this.monitoredTables = Collections.synchronizedSet(new HashSet<>());
this.config = PixelsSinkConfigFactory.getInstance();
- this.trinoUser = config.getTrinoUser();
- this.trinoJdbcUrl = config.getTrinoUrl();
- this.trinoPassword = config.getTrinoPassword();
+ this.queryUser = config.getSinkQueryUser();
+ this.queryJdbcUrl = config.getSinkQueryUrl();
+ this.queryPassword = config.getSinkQueryPassword();
+ this.queryEngine = resolveQueryEngine(queryJdbcUrl);
this.warmUpSeconds = config.getSinkMonitorFreshnessEmbedWarmupSeconds();
- this.maxConcurrentQueries = config.getTrinoParallel();
+ this.maxConcurrentQueries = config.getSinkQueryParallel();
this.queryPermits = new Semaphore(maxConcurrentQueries);
// Initializes a single-threaded scheduler for executing freshness queries
this.scheduler = Executors.newSingleThreadScheduledExecutor(r ->
@@ -113,42 +127,59 @@ public static FreshnessClient getInstance()
return instance;
}
- @Deprecated
- protected Connection createNewConnection() throws SQLException
+ /**
+ * Unified JDBC entry: resolve engine from URL at construction, load driver,
+ * apply engine-specific connection properties, then return a ready Connection.
+ *
+ * @param queryTimestamp Trino snapshot timestamp when embed.snapshot is enabled; ignored otherwise / for Hive
+ */
+ protected Connection openConnection(Long queryTimestamp) throws SQLException
{
try
{
- Class.forName("io.trino.jdbc.TrinoDriver");
- } catch (ClassNotFoundException e)
+ Class.forName(queryEngine.driverClass);
+ }
+ catch (ClassNotFoundException e)
{
- throw new SQLException(e);
+ throw new SQLException(queryEngine.missingHint, e);
}
Properties properties = new Properties();
+ properties.setProperty("user", queryUser);
+ if (queryEngine == QueryEngine.TRINO
+ && queryTimestamp != null
+ && config.isSinkMonitorFreshnessEmbedSnapshot())
+ {
+ String sessionPropValue = String.format(
+ "pixels.query_snapshot_timestamp:%d", queryTimestamp);
+ properties.setProperty("sessionProperties", sessionPropValue);
+ }
+ return DriverManager.getConnection(queryJdbcUrl, properties);
+ }
-
- return DriverManager.getConnection(trinoJdbcUrl, trinoUser, null);
+ @Deprecated
+ protected Connection createNewConnection() throws SQLException
+ {
+ return openConnection(null);
}
protected Connection createNewConnection(long queryTimestamp) throws SQLException
{
- try
- {
- Class.forName("io.trino.jdbc.TrinoDriver");
- } catch (ClassNotFoundException e)
+ return openConnection(queryTimestamp);
+ }
+
+ private static QueryEngine resolveQueryEngine(String jdbcUrl)
+ {
+ if (jdbcUrl != null && jdbcUrl.startsWith("jdbc:hive2:"))
{
- throw new SQLException(e);
+ return QueryEngine.HIVE;
}
-
- Properties properties = new Properties();
- properties.setProperty("user", trinoUser);
- if (config.isSinkMonitorFreshnessEmbedSnapshot())
+ if (jdbcUrl != null && jdbcUrl.startsWith("jdbc:trino:"))
{
- String catalogName = "pixels";
- String sessionPropValue = String.format("%s.query_snapshot_timestamp:%d", catalogName, queryTimestamp);
- properties.setProperty("sessionProperties", sessionPropValue);
+ return QueryEngine.TRINO;
}
- return DriverManager.getConnection(trinoJdbcUrl, properties);
+ throw new IllegalArgumentException(
+ "Unsupported freshness JDBC URL (expect jdbc:trino:// or jdbc:hive2://): " + jdbcUrl);
}
private void closeConnection(Connection conn)
@@ -160,7 +191,7 @@ private void closeConnection(Connection conn)
conn.close();
} catch (SQLException e)
{
- LOGGER.warn("Error closing Trino connection.", e);
+ LOGGER.warn("Error closing freshness JDBC connection.", e);
}
}
}
@@ -282,14 +313,13 @@ void queryAndCalculateFreshness()
}
// Timestamp when the query is sent (t_send)
long tSendMillis = System.currentTimeMillis();
+ Long snapshotTimestamp = null;
if (config.isSinkMonitorFreshnessEmbedSnapshot())
{
transContext = TransService.Instance().beginTrans(true);
- conn = createNewConnection(transContext.getTimestamp());
- } else
- {
- conn = createNewConnection();
+ snapshotTimestamp = transContext.getTimestamp();
}
+ conn = openConnection(snapshotTimestamp);
String tSendMillisStr = DateUtil.convertDateToString(new Date(tSendMillis));
// Query to find the latest timestamp in the table
diff --git a/src/main/java/io/pixelsdb/pixels/sink/freshness/FreshnessHistory.java b/src/main/java/io/pixelsdb/pixels/sink/freshness/FreshnessHistory.java
index 1037f2e..2843b2c 100644
--- a/src/main/java/io/pixelsdb/pixels/sink/freshness/FreshnessHistory.java
+++ b/src/main/java/io/pixelsdb/pixels/sink/freshness/FreshnessHistory.java
@@ -1,23 +1,18 @@
/*
* Copyright 2025 PixelsDB.
*
- * This file is part of Pixels.
+ * Licensed 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
*
- * Pixels is free software: you can redistribute it and/or modify
- * it under the terms of the Affero GNU General Public License as
- * published by the Free Software Foundation, either version 3 of
- * the License, or (at your option) any later version.
+ * http://www.apache.org/licenses/LICENSE-2.0
*
- * Pixels is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * Affero GNU General Public License for more details.
- *
- * You should have received a copy of the Affero GNU General Public
- * License along with Pixels. If not, see
- * .
+ * 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 io.pixelsdb.pixels.sink.freshness;
diff --git a/src/main/java/io/pixelsdb/pixels/sink/freshness/OneSecondAverage.java b/src/main/java/io/pixelsdb/pixels/sink/freshness/OneSecondAverage.java
index cd4f3c2..b017536 100644
--- a/src/main/java/io/pixelsdb/pixels/sink/freshness/OneSecondAverage.java
+++ b/src/main/java/io/pixelsdb/pixels/sink/freshness/OneSecondAverage.java
@@ -1,23 +1,18 @@
/*
* Copyright 2025 PixelsDB.
*
- * This file is part of Pixels.
+ * Licensed 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
*
- * Pixels is free software: you can redistribute it and/or modify
- * it under the terms of the Affero GNU General Public License as
- * published by the Free Software Foundation, either version 3 of
- * the License, or (at your option) any later version.
+ * http://www.apache.org/licenses/LICENSE-2.0
*
- * Pixels is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * Affero GNU General Public License for more details.
- *
- * You should have received a copy of the Affero GNU General Public
- * License along with Pixels. If not, see
- * .
+ * 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 io.pixelsdb.pixels.sink.freshness;
import java.util.ArrayDeque;
diff --git a/src/main/java/io/pixelsdb/pixels/sink/metadata/TableMetadata.java b/src/main/java/io/pixelsdb/pixels/sink/metadata/TableMetadata.java
index 0935d7a..63e1d16 100644
--- a/src/main/java/io/pixelsdb/pixels/sink/metadata/TableMetadata.java
+++ b/src/main/java/io/pixelsdb/pixels/sink/metadata/TableMetadata.java
@@ -1,23 +1,18 @@
/*
* Copyright 2025 PixelsDB.
*
- * This file is part of Pixels.
+ * Licensed 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
*
- * Pixels is free software: you can redistribute it and/or modify
- * it under the terms of the Affero GNU General Public License as
- * published by the Free Software Foundation, either version 3 of
- * the License, or (at your option) any later version.
+ * http://www.apache.org/licenses/LICENSE-2.0
*
- * Pixels is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * Affero GNU General Public License for more details.
- *
- * You should have received a copy of the Affero GNU General Public
- * License along with Pixels. If not, see
- * .
+ * 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 io.pixelsdb.pixels.sink.metadata;
import io.pixelsdb.pixels.common.exception.MetadataException;
diff --git a/src/main/java/io/pixelsdb/pixels/sink/metadata/TableMetadataRegistry.java b/src/main/java/io/pixelsdb/pixels/sink/metadata/TableMetadataRegistry.java
index c776e71..9b6121e 100644
--- a/src/main/java/io/pixelsdb/pixels/sink/metadata/TableMetadataRegistry.java
+++ b/src/main/java/io/pixelsdb/pixels/sink/metadata/TableMetadataRegistry.java
@@ -1,23 +1,18 @@
/*
* Copyright 2025 PixelsDB.
*
- * This file is part of Pixels.
+ * Licensed 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
*
- * Pixels is free software: you can redistribute it and/or modify
- * it under the terms of the Affero GNU General Public License as
- * published by the Free Software Foundation, either version 3 of
- * the License, or (at your option) any later version.
+ * http://www.apache.org/licenses/LICENSE-2.0
*
- * Pixels is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * Affero GNU General Public License for more details.
- *
- * You should have received a copy of the Affero GNU General Public
- * License along with Pixels. If not, see
- * .
+ * 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 io.pixelsdb.pixels.sink.metadata;
import io.pixelsdb.pixels.common.exception.MetadataException;
diff --git a/src/main/java/io/pixelsdb/pixels/sink/pipeline/TablePipeline.java b/src/main/java/io/pixelsdb/pixels/sink/pipeline/TablePipeline.java
new file mode 100644
index 0000000..4580679
--- /dev/null
+++ b/src/main/java/io/pixelsdb/pixels/sink/pipeline/TablePipeline.java
@@ -0,0 +1,74 @@
+/*
+ * Copyright 2026 PixelsDB.
+ *
+ * Licensed 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 io.pixelsdb.pixels.sink.pipeline;
+
+import io.pixelsdb.pixels.sink.config.PixelsSinkConstants;
+import io.pixelsdb.pixels.sink.event.RowChangeEvent;
+import io.pixelsdb.pixels.sink.processor.TableProcessor;
+import io.pixelsdb.pixels.sink.util.BlockingBoundedQueue;
+import io.pixelsdb.pixels.sink.writer.PixelsSinkWriter;
+import io.pixelsdb.pixels.sink.writer.PixelsSinkWriterFactory;
+
+public final class TablePipeline implements AutoCloseable
+{
+ private final BlockingBoundedQueue eventQueue;
+ private final PixelsSinkWriter writer;
+ private final TableProcessor processor;
+
+ public TablePipeline()
+ {
+ this.eventQueue = new BlockingBoundedQueue<>(PixelsSinkConstants.MAX_QUEUE_SIZE);
+ this.writer = PixelsSinkWriterFactory.getWriter();
+ this.processor = new TableProcessor(eventQueue, writer);
+ }
+
+ public void start()
+ {
+ processor.run();
+ }
+
+ public void publish(RowChangeEvent event)
+ {
+ eventQueue.put(event);
+ }
+
+ /**
+ * Stops accepting events and waits for all pending events to be written.
+ */
+ @Override
+ public void close()
+ {
+ eventQueue.close();
+ try
+ {
+ processor.awaitTermination();
+ } catch (InterruptedException e)
+ {
+ abort();
+ Thread.currentThread().interrupt();
+ }
+ }
+
+ /**
+ * Discards pending events and interrupts processing. Already written events
+ * are not rolled back.
+ */
+ public void abort()
+ {
+ processor.abort();
+ eventQueue.abort();
+ }
+}
diff --git a/src/main/java/io/pixelsdb/pixels/sink/pipeline/TablePipelineManager.java b/src/main/java/io/pixelsdb/pixels/sink/pipeline/TablePipelineManager.java
new file mode 100644
index 0000000..bec3ace
--- /dev/null
+++ b/src/main/java/io/pixelsdb/pixels/sink/pipeline/TablePipelineManager.java
@@ -0,0 +1,91 @@
+/*
+ * Copyright 2026 PixelsDB.
+ *
+ * Licensed 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 io.pixelsdb.pixels.sink.pipeline;
+
+import io.pixelsdb.pixels.common.metadata.SchemaTableName;
+import io.pixelsdb.pixels.sink.event.RowChangeEvent;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+public final class TablePipelineManager implements AutoCloseable
+{
+ private final Map pipelines = new ConcurrentHashMap<>();
+ private final Object lifecycleLock = new Object();
+ private volatile boolean closed;
+
+ public void route(RowChangeEvent event)
+ {
+ if (event == null || closed)
+ {
+ return;
+ }
+ SchemaTableName table = new SchemaTableName(event.getSchemaName(), event.getTable());
+ TablePipeline pipeline = pipelines.get(table);
+ if (pipeline == null)
+ {
+ synchronized (lifecycleLock)
+ {
+ if (closed)
+ {
+ return;
+ }
+ pipeline = pipelines.computeIfAbsent(table, ignored ->
+ {
+ TablePipeline newPipeline = new TablePipeline();
+ newPipeline.start();
+ return newPipeline;
+ });
+ }
+ }
+ pipeline.publish(event);
+ }
+
+ @Override
+ public void close()
+ {
+ List pipelinesToClose;
+ synchronized (lifecycleLock)
+ {
+ if (closed)
+ {
+ return;
+ }
+ closed = true;
+ pipelinesToClose = new ArrayList<>(pipelines.values());
+ pipelines.clear();
+ }
+ pipelinesToClose.forEach(TablePipeline::close);
+ }
+
+ public void abort()
+ {
+ List pipelinesToAbort;
+ synchronized (lifecycleLock)
+ {
+ if (closed)
+ {
+ return;
+ }
+ closed = true;
+ pipelinesToAbort = new ArrayList<>(pipelines.values());
+ pipelines.clear();
+ }
+ pipelinesToAbort.forEach(TablePipeline::abort);
+ }
+}
diff --git a/src/main/java/io/pixelsdb/pixels/sink/pipeline/TransactionPipeline.java b/src/main/java/io/pixelsdb/pixels/sink/pipeline/TransactionPipeline.java
new file mode 100644
index 0000000..696c002
--- /dev/null
+++ b/src/main/java/io/pixelsdb/pixels/sink/pipeline/TransactionPipeline.java
@@ -0,0 +1,81 @@
+/*
+ * Copyright 2026 PixelsDB.
+ *
+ * Licensed 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 io.pixelsdb.pixels.sink.pipeline;
+
+import io.pixelsdb.pixels.sink.SinkProto;
+import io.pixelsdb.pixels.sink.config.PixelsSinkConstants;
+import io.pixelsdb.pixels.sink.processor.TransactionProcessor;
+import io.pixelsdb.pixels.sink.util.BlockingBoundedQueue;
+import io.pixelsdb.pixels.sink.writer.PixelsSinkWriter;
+import io.pixelsdb.pixels.sink.writer.PixelsSinkWriterFactory;
+
+public final class TransactionPipeline implements AutoCloseable
+{
+ private final BlockingBoundedQueue eventQueue;
+ private final PixelsSinkWriter writer;
+ private final TransactionProcessor processor;
+ private final Thread processorThread;
+
+ public TransactionPipeline()
+ {
+ this.eventQueue = new BlockingBoundedQueue<>(PixelsSinkConstants.MAX_QUEUE_SIZE);
+ this.writer = PixelsSinkWriterFactory.getWriter();
+ this.processor = new TransactionProcessor(eventQueue, writer);
+ this.processorThread = new Thread(processor, "transaction-processor");
+ }
+
+ public void start()
+ {
+ if (!processorThread.isAlive())
+ {
+ processorThread.start();
+ }
+ }
+
+ public void publish(SinkProto.TransactionMetadata transaction)
+ {
+ eventQueue.put(transaction);
+ }
+
+ /**
+ * Stops accepting transactions and waits for all pending transactions to
+ * be written.
+ */
+ @Override
+ public void close()
+ {
+ eventQueue.close();
+ try
+ {
+ processorThread.join();
+ } catch (InterruptedException e)
+ {
+ abort();
+ Thread.currentThread().interrupt();
+ }
+ }
+
+ /**
+ * Discards pending transactions and interrupts processing. Already written
+ * transactions are not rolled back.
+ */
+ public void abort()
+ {
+ processor.abort();
+ eventQueue.abort();
+ processorThread.interrupt();
+ }
+}
diff --git a/src/main/java/io/pixelsdb/pixels/sink/processor/MonitorThreadManager.java b/src/main/java/io/pixelsdb/pixels/sink/processor/MonitorThreadManager.java
deleted file mode 100644
index a3686a4..0000000
--- a/src/main/java/io/pixelsdb/pixels/sink/processor/MonitorThreadManager.java
+++ /dev/null
@@ -1,78 +0,0 @@
-/*
- * Copyright 2025 PixelsDB.
- *
- * This file is part of Pixels.
- *
- * Pixels is free software: you can redistribute it and/or modify
- * it under the terms of the Affero GNU General Public License as
- * published by the Free Software Foundation, either version 3 of
- * the License, or (at your option) any later version.
- *
- * Pixels is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * Affero GNU General Public License for more details.
- *
- * You should have received a copy of the Affero GNU General Public
- * License along with Pixels. If not, see
- * .
- */
-
-package io.pixelsdb.pixels.sink.processor;
-
-import io.pixelsdb.pixels.sink.config.PixelsSinkConstants;
-
-import java.util.List;
-import java.util.concurrent.CopyOnWriteArrayList;
-import java.util.concurrent.ExecutorService;
-import java.util.concurrent.Executors;
-import java.util.concurrent.TimeUnit;
-
-public class MonitorThreadManager
-{
- private final List monitors = new CopyOnWriteArrayList<>();
- private final ExecutorService executor = Executors.newFixedThreadPool(PixelsSinkConstants.MONITOR_NUM);
-
- public void startMonitor(Runnable monitor)
- {
- monitors.add(monitor);
- executor.submit(monitor);
- }
-
- public void shutdown()
- {
- stopMonitors();
- shutdownExecutor();
- awaitTermination();
- }
-
- private void stopMonitors()
- {
- monitors.forEach(monitor ->
- {
- if (monitor instanceof StoppableProcessor)
- {
- ((StoppableProcessor) monitor).stopProcessor();
- }
- });
- }
-
- private void shutdownExecutor()
- {
- executor.shutdown();
- }
-
- private void awaitTermination()
- {
- try
- {
- if (!executor.awaitTermination(10, TimeUnit.SECONDS))
- {
- executor.shutdownNow();
- }
- } catch (InterruptedException e)
- {
- executor.shutdownNow();
- }
- }
-}
diff --git a/src/main/java/io/pixelsdb/pixels/sink/processor/StoppableProcessor.java b/src/main/java/io/pixelsdb/pixels/sink/processor/StoppableProcessor.java
deleted file mode 100644
index 9c42a8d..0000000
--- a/src/main/java/io/pixelsdb/pixels/sink/processor/StoppableProcessor.java
+++ /dev/null
@@ -1,27 +0,0 @@
-/*
- * Copyright 2025 PixelsDB.
- *
- * This file is part of Pixels.
- *
- * Pixels is free software: you can redistribute it and/or modify
- * it under the terms of the Affero GNU General Public License as
- * published by the Free Software Foundation, either version 3 of
- * the License, or (at your option) any later version.
- *
- * Pixels is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * Affero GNU General Public License for more details.
- *
- * You should have received a copy of the Affero GNU General Public
- * License along with Pixels. If not, see
- * .
- */
-
-
-package io.pixelsdb.pixels.sink.processor;
-
-public interface StoppableProcessor
-{
- void stopProcessor();
-}
diff --git a/src/main/java/io/pixelsdb/pixels/sink/processor/TableProcessor.java b/src/main/java/io/pixelsdb/pixels/sink/processor/TableProcessor.java
index 134dab7..98698fc 100644
--- a/src/main/java/io/pixelsdb/pixels/sink/processor/TableProcessor.java
+++ b/src/main/java/io/pixelsdb/pixels/sink/processor/TableProcessor.java
@@ -1,28 +1,23 @@
/*
* Copyright 2025 PixelsDB.
*
- * This file is part of Pixels.
+ * Licensed 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
*
- * Pixels is free software: you can redistribute it and/or modify
- * it under the terms of the Affero GNU General Public License as
- * published by the Free Software Foundation, either version 3 of
- * the License, or (at your option) any later version.
+ * http://www.apache.org/licenses/LICENSE-2.0
*
- * Pixels is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * Affero GNU General Public License for more details.
- *
- * You should have received a copy of the Affero GNU General Public
- * License along with Pixels. If not, see
- * .
+ * 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 io.pixelsdb.pixels.sink.processor;
import io.pixelsdb.pixels.sink.event.RowChangeEvent;
-import io.pixelsdb.pixels.sink.provider.TableEventProvider;
+import io.pixelsdb.pixels.sink.util.BlockingBoundedQueue;
import io.pixelsdb.pixels.sink.util.MetricsFacade;
import io.pixelsdb.pixels.sink.writer.PixelsSinkWriter;
import io.pixelsdb.pixels.sink.writer.PixelsSinkWriterFactory;
@@ -37,20 +32,27 @@
* @author: AntiO2
* @date: 2025/9/26 11:01
*/
-public class TableProcessor implements StoppableProcessor, Runnable
+public class TableProcessor implements Runnable
{
private static final Logger LOGGER = LoggerFactory.getLogger(TableProcessor.class);
private final AtomicBoolean running = new AtomicBoolean(true);
private final PixelsSinkWriter pixelsSinkWriter;
- private final TableEventProvider> tableEventProvider;
+ private final BlockingBoundedQueue eventQueue;
private final MetricsFacade metricsFacade = MetricsFacade.getInstance();
private Thread processorThread;
private boolean tableAdded = false;
- public TableProcessor(TableEventProvider> tableEventProvider)
+ public TableProcessor(BlockingBoundedQueue eventQueue)
{
- this.pixelsSinkWriter = PixelsSinkWriterFactory.getWriter();
- this.tableEventProvider = tableEventProvider;
+ this(eventQueue, PixelsSinkWriterFactory.getWriter());
+ }
+
+ public TableProcessor(
+ BlockingBoundedQueue eventQueue,
+ PixelsSinkWriter pixelsSinkWriter)
+ {
+ this.pixelsSinkWriter = pixelsSinkWriter;
+ this.eventQueue = eventQueue;
}
@Override
@@ -64,21 +66,32 @@ private void processLoop()
{
while (running.get())
{
- RowChangeEvent event = tableEventProvider.getRowChangeEvent();
+ RowChangeEvent event = eventQueue.take();
if (event == null)
{
- continue;
+ break;
}
pixelsSinkWriter.writeRow(event);
}
+ running.set(false);
LOGGER.info("Processor thread exited");
}
- @Override
- public void stopProcessor()
+ public void awaitTermination() throws InterruptedException
{
- LOGGER.info("Stopping transaction monitor");
+ if (processorThread != null)
+ {
+ processorThread.join();
+ }
+ }
+
+ public void abort()
+ {
+ LOGGER.info("Aborting table processor");
running.set(false);
- processorThread.interrupt();
+ if (processorThread != null)
+ {
+ processorThread.interrupt();
+ }
}
}
diff --git a/src/main/java/io/pixelsdb/pixels/sink/processor/TopicProcessor.java b/src/main/java/io/pixelsdb/pixels/sink/processor/TopicProcessor.java
deleted file mode 100644
index 905c87e..0000000
--- a/src/main/java/io/pixelsdb/pixels/sink/processor/TopicProcessor.java
+++ /dev/null
@@ -1,285 +0,0 @@
-/*
- * Copyright 2025 PixelsDB.
- *
- * This file is part of Pixels.
- *
- * Pixels is free software: you can redistribute it and/or modify
- * it under the terms of the Affero GNU General Public License as
- * published by the Free Software Foundation, either version 3 of
- * the License, or (at your option) any later version.
- *
- * Pixels is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * Affero GNU General Public License for more details.
- *
- * You should have received a copy of the Affero GNU General Public
- * License along with Pixels. If not, see
- * .
- */
-
-package io.pixelsdb.pixels.sink.processor;
-
-import io.pixelsdb.pixels.sink.config.PixelsSinkConfig;
-import io.pixelsdb.pixels.sink.provider.TableEventKafkaProvider;
-import org.apache.kafka.clients.admin.AdminClient;
-import org.apache.kafka.clients.admin.AdminClientConfig;
-import org.apache.kafka.clients.admin.ListTopicsResult;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import java.io.IOException;
-import java.time.Duration;
-import java.util.*;
-import java.util.concurrent.*;
-import java.util.concurrent.atomic.AtomicBoolean;
-import java.util.stream.Collectors;
-
-public class TopicProcessor implements StoppableProcessor, Runnable
-{
-
- private static final Logger log = LoggerFactory.getLogger(TopicProcessor.class);
- private final Properties kafkaProperties;
- private final PixelsSinkConfig pixelsSinkConfig;
- private final String[] includeTables;
- private final Set subscribedTopics = ConcurrentHashMap.newKeySet();
- private final String bootstrapServers;
- private final String baseTopic;
-
- private final AtomicBoolean running = new AtomicBoolean(true);
- private final Map activeTasks = new ConcurrentHashMap<>(); // track row event consumer
- private final ExecutorService executorService = Executors.newCachedThreadPool();
- private AdminClient adminClient;
- private Timer timer;
-
- public TopicProcessor(PixelsSinkConfig pixelsSinkConfig, Properties kafkaProperties)
- {
- this.pixelsSinkConfig = pixelsSinkConfig;
- this.kafkaProperties = kafkaProperties;
- this.baseTopic = pixelsSinkConfig.getTopicPrefix() + "." + pixelsSinkConfig.getCaptureDatabase();
- this.includeTables = pixelsSinkConfig.getIncludeTables();
- this.bootstrapServers = pixelsSinkConfig.getBootstrapServers();
- }
-
- private static Set filterTopics(Set topics, String prefix)
- {
- return topics.stream()
- .filter(t -> t.startsWith(prefix))
- .collect(Collectors.toSet());
- }
-
- @Override
- public void run()
- {
- try
- {
- initializeResources();
- startMonitoringCycle();
- } finally
- {
- cleanupResources();
- log.info("Topic monitor stopped");
- }
- }
-
- @Override
- public void stopProcessor()
- {
- log.info("Initiating topic monitor shutdown...");
- running.set(false);
- interruptMonitoring();
- shutdownConsumerTasks();
- awaitTermination();
- }
-
- private void shutdownConsumerTasks()
- {
- log.info("Shutting down {} active consumer tasks", activeTasks.size());
- activeTasks.forEach((topic, task) ->
- {
- log.info("Stopping consumer for topic: {}", topic);
- task.close();
- });
- activeTasks.clear();
- }
-
- private void awaitTermination()
- {
- try
- {
- if (executorService != null && !executorService.awaitTermination(30, TimeUnit.SECONDS))
- {
- log.warn("Forcing shutdown of remaining tasks");
- executorService.shutdownNow();
- }
- } catch (InterruptedException e)
- {
- Thread.currentThread().interrupt();
- }
- }
-
- private void initializeResources()
- {
- Properties props = new Properties();
- props.put(AdminClientConfig.BOOTSTRAP_SERVERS_CONFIG, this.bootstrapServers);
- this.adminClient = AdminClient.create(props);
- this.timer = new Timer("TopicProcessor-Timer", true);
- log.info("Started topic monitor for base topic: {}", baseTopic);
- }
-
- private void startMonitoringCycle()
- {
- String topicPrefix = baseTopic + ".";
- timer.scheduleAtFixedRate(new TopicMonitorTask(), 0, 5000);
-
- while (running.get())
- {
- try
- {
- TimeUnit.SECONDS.sleep(1);
- } catch (InterruptedException e)
- {
- if (running.get())
- {
- log.warn("Monitoring thread interrupted unexpectedly", e);
- }
- Thread.currentThread().interrupt();
- }
- }
- }
-
- private void interruptMonitoring()
- {
- if (timer != null)
- {
- timer.cancel();
- timer.purge();
- }
- if (adminClient != null)
- {
- adminClient.close(Duration.ofSeconds(5));
- }
- shutdownExecutorService();
- }
-
- private void shutdownExecutorService()
- {
- executorService.shutdown();
- try
- {
- if (!executorService.awaitTermination(10, TimeUnit.SECONDS))
- {
- executorService.shutdownNow();
- }
- } catch (InterruptedException e)
- {
- executorService.shutdownNow();
- Thread.currentThread().interrupt();
- }
- }
-
- private void cleanupResources()
- {
- try
- {
- if (adminClient != null)
- {
- adminClient.close(Duration.ofSeconds(5));
- }
- } catch (Exception e)
- {
- log.warn("Error closing admin client", e);
- }
- }
-
- private Set detectNewTopics(Set currentTopics)
- {
- return currentTopics.stream()
- .filter(t -> !subscribedTopics.contains(t))
- .collect(Collectors.toSet());
- }
-
- private String extractTableName(String topic)
- {
- int lastDotIndex = topic.lastIndexOf('.');
- return lastDotIndex != -1 ? topic.substring(lastDotIndex + 1) : topic;
- }
-
- private void launchConsumerTask(String topic)
- {
- try
- {
- TableEventKafkaProvider task = new TableEventKafkaProvider(kafkaProperties, topic);
- executorService.submit(task);
- } catch (Exception e)
- {
- log.error("Failed to start consumer for topic {}: {}", topic, e.getMessage());
- }
- }
-
- private class TopicMonitorTask extends TimerTask
- {
- @Override
- public void run()
- {
- if (!running.get())
- {
- cancel();
- return;
- }
-
- try
- {
- processTopicChanges();
- } catch (Exception e)
- {
- e.printStackTrace();
- log.error("Error processing topic changes: {}", e.getMessage());
- }
- }
-
- private void processTopicChanges()
- {
- try
- {
- ListTopicsResult listTopicsResult = adminClient.listTopics();
- Set currentTopics = listTopicsResult.names().get(5, TimeUnit.SECONDS);
- Set filteredTopics = filterTopics(currentTopics, baseTopic + ".");
-
- Set newTopics = detectNewTopics(filteredTopics);
- handleNewTopics(newTopics);
- } catch (TimeoutException | ExecutionException | InterruptedException ignored)
- {
-
- }
- }
-
- private void handleNewTopics(Set newTopics)
- {
- newTopics.stream()
- .filter(this::shouldProcessTable)
- .forEach(topic ->
- {
- try
- {
- TableEventKafkaProvider task = new TableEventKafkaProvider(kafkaProperties, topic);
- executorService.submit(task);
- activeTasks.put(topic, task);
- subscribedTopics.add(topic);
- } catch (IOException e)
- {
- log.error("Failed to create consumer for {}: {}", topic, e.getMessage());
- }
- });
- }
-
- private boolean shouldProcessTable(String topic)
- {
- String tableName = extractTableName(topic);
- return includeTables.length == 0 ||
- Arrays.stream(includeTables).anyMatch(t -> t.equals(tableName));
- }
- }
-
-}
-
diff --git a/src/main/java/io/pixelsdb/pixels/sink/processor/TransactionProcessor.java b/src/main/java/io/pixelsdb/pixels/sink/processor/TransactionProcessor.java
index 1d28fb1..3a298ff 100644
--- a/src/main/java/io/pixelsdb/pixels/sink/processor/TransactionProcessor.java
+++ b/src/main/java/io/pixelsdb/pixels/sink/processor/TransactionProcessor.java
@@ -1,27 +1,22 @@
/*
* Copyright 2025 PixelsDB.
*
- * This file is part of Pixels.
+ * Licensed 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
*
- * Pixels is free software: you can redistribute it and/or modify
- * it under the terms of the Affero GNU General Public License as
- * published by the Free Software Foundation, either version 3 of
- * the License, or (at your option) any later version.
+ * http://www.apache.org/licenses/LICENSE-2.0
*
- * Pixels is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * Affero GNU General Public License for more details.
- *
- * You should have received a copy of the Affero GNU General Public
- * License along with Pixels. If not, see
- * .
+ * 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 io.pixelsdb.pixels.sink.processor;
import io.pixelsdb.pixels.sink.SinkProto;
-import io.pixelsdb.pixels.sink.provider.TransactionEventProvider;
+import io.pixelsdb.pixels.sink.util.BlockingBoundedQueue;
import io.pixelsdb.pixels.sink.writer.PixelsSinkWriter;
import io.pixelsdb.pixels.sink.writer.PixelsSinkWriterFactory;
import org.slf4j.Logger;
@@ -29,17 +24,25 @@
import java.util.concurrent.atomic.AtomicBoolean;
-public class TransactionProcessor implements Runnable, StoppableProcessor
+public class TransactionProcessor implements Runnable
{
private static final Logger LOGGER = LoggerFactory.getLogger(TransactionProcessor.class);
private final PixelsSinkWriter sinkWriter;
private final AtomicBoolean running = new AtomicBoolean(true);
- private final TransactionEventProvider transactionEventProvider;
+ private final BlockingBoundedQueue eventQueue;
+
+ public TransactionProcessor(
+ BlockingBoundedQueue eventQueue)
+ {
+ this(eventQueue, PixelsSinkWriterFactory.getWriter());
+ }
- public TransactionProcessor(TransactionEventProvider transactionEventProvider)
+ public TransactionProcessor(
+ BlockingBoundedQueue eventQueue,
+ PixelsSinkWriter sinkWriter)
{
- this.transactionEventProvider = transactionEventProvider;
- this.sinkWriter = PixelsSinkWriterFactory.getWriter();
+ this.eventQueue = eventQueue;
+ this.sinkWriter = sinkWriter;
}
@Override
@@ -47,10 +50,9 @@ public void run()
{
while (running.get())
{
- SinkProto.TransactionMetadata transaction = transactionEventProvider.getTransaction();
+ SinkProto.TransactionMetadata transaction = eventQueue.take();
if (transaction == null)
{
- LOGGER.warn("Received null transaction");
running.set(false);
break;
}
@@ -59,10 +61,9 @@ public void run()
LOGGER.info("Processor thread exited for transaction");
}
- @Override
- public void stopProcessor()
+ public void abort()
{
- LOGGER.info("Stopping transaction monitor");
+ LOGGER.info("Aborting transaction processor");
running.set(false);
}
}
diff --git a/src/main/java/io/pixelsdb/pixels/sink/provider/EventProvider.java b/src/main/java/io/pixelsdb/pixels/sink/provider/EventProvider.java
deleted file mode 100644
index 8039405..0000000
--- a/src/main/java/io/pixelsdb/pixels/sink/provider/EventProvider.java
+++ /dev/null
@@ -1,185 +0,0 @@
-/*
- * Copyright 2025 PixelsDB.
- *
- * This file is part of Pixels.
- *
- * Pixels is free software: you can redistribute it and/or modify
- * it under the terms of the Affero GNU General Public License as
- * published by the Free Software Foundation, either version 3 of
- * the License, or (at your option) any later version.
- *
- * Pixels is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * Affero GNU General Public License for more details.
- *
- * You should have received a copy of the Affero GNU General Public
- * License along with Pixels. If not, see
- * .
- */
-
-package io.pixelsdb.pixels.sink.provider;
-
-import io.pixelsdb.pixels.sink.config.PixelsSinkConstants;
-import io.pixelsdb.pixels.sink.util.MetricsFacade;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import java.io.Closeable;
-import java.util.ArrayList;
-import java.util.List;
-import java.util.concurrent.*;
-
-public abstract class EventProvider implements Runnable, Closeable
-{
- private static final Logger LOGGER = LoggerFactory.getLogger(EventProvider.class);
-
- private static final int BATCH_SIZE = 64;
- private static final int THREAD_NUM = 4;
- private static final long MAX_WAIT_MS = 5; // configurable
- protected final MetricsFacade metricsFacade = MetricsFacade.getInstance();
- private final BlockingQueue rawEventQueue = new LinkedBlockingQueue<>(PixelsSinkConstants.MAX_QUEUE_SIZE);
- private final BlockingQueue eventQueue = new LinkedBlockingQueue<>(PixelsSinkConstants.MAX_QUEUE_SIZE);
- private final ExecutorService decodeExecutor = Executors.newFixedThreadPool(THREAD_NUM);
-
- private Thread providerThread;
-
-
- @Override
- public void run()
- {
- providerThread = new Thread(this::processLoop);
- providerThread.start();
- }
-
- @Override
- public void close()
- {
- this.providerThread.interrupt();
- decodeExecutor.shutdown();
- }
-
- protected void processLoop()
- {
- List sourceBatch = new ArrayList<>(BATCH_SIZE);
- while (true)
- {
- try
- {
- sourceBatch.clear();
- // take first element (blocking)
- SOURCE_RECORD_T first = getRawEvent();
- sourceBatch.add(first);
- long startTime = System.nanoTime();
-
- // keep polling until sourceBatch full or timeout
- while (sourceBatch.size() < BATCH_SIZE)
- {
- long elapsedMs = (System.nanoTime() - startTime) / 1_000_000;
- long remainingMs = MAX_WAIT_MS - elapsedMs;
- if (remainingMs <= 0)
- {
- break;
- }
-
- SOURCE_RECORD_T next = pollRawEvent(remainingMs);
- if (next == null)
- {
- break;
- }
- sourceBatch.add(next);
- }
-
- // parallel decode
- List> futures = new ArrayList<>(sourceBatch.size());
- for (SOURCE_RECORD_T data : sourceBatch)
- {
- futures.add(decodeExecutor.submit(() ->
- convertToTargetRecord(data)));
- }
-
- // ordered put into queue
- for (Future future : futures)
- {
- try
- {
- TARGET_RECORD_T event = future.get();
- if (event != null)
- {
- recordSerdEvent();
- putTargetEvent(event);
- }
- } catch (ExecutionException e)
- {
- LOGGER.warn("Decode failed: {}", String.valueOf(e.getCause()));
- }
- }
- } catch (InterruptedException e)
- {
- Thread.currentThread().interrupt();
- break;
- }
- }
- }
-
- abstract TARGET_RECORD_T convertToTargetRecord(SOURCE_RECORD_T record);
-
- protected TARGET_RECORD_T getTargetEvent()
- {
- try
- {
- return eventQueue.take();
- } catch (InterruptedException e)
- {
- Thread.currentThread().interrupt();
- }
- return null;
- }
-
- protected void putTargetEvent(TARGET_RECORD_T event)
- {
- try
- {
- eventQueue.put(event);
- } catch (InterruptedException e)
- {
- Thread.currentThread().interrupt();
- }
- }
-
- protected void putRawEvent(SOURCE_RECORD_T record)
- {
- try
- {
- rawEventQueue.put(record);
- } catch (InterruptedException e)
- {
- Thread.currentThread().interrupt();
- }
- }
-
- protected SOURCE_RECORD_T getRawEvent()
- {
- try
- {
- return rawEventQueue.take();
- } catch (InterruptedException e)
- {
- Thread.currentThread().interrupt();
- return null;
- }
- }
-
- protected SOURCE_RECORD_T pollRawEvent(long remainingMs)
- {
- try
- {
- return rawEventQueue.poll(remainingMs, TimeUnit.MILLISECONDS);
- } catch (InterruptedException e)
- {
- return null;
- }
- }
-
- abstract protected void recordSerdEvent();
-}
diff --git a/src/main/java/io/pixelsdb/pixels/sink/provider/ProtoType.java b/src/main/java/io/pixelsdb/pixels/sink/provider/ProtoType.java
index ea7a4bf..5eaa4c1 100644
--- a/src/main/java/io/pixelsdb/pixels/sink/provider/ProtoType.java
+++ b/src/main/java/io/pixelsdb/pixels/sink/provider/ProtoType.java
@@ -1,23 +1,18 @@
/*
- * Copyright 2025 PixelsDB.
+ * Copyright 2023 PixelsDB.
*
- * This file is part of Pixels.
+ * Licensed 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
*
- * Pixels is free software: you can redistribute it and/or modify
- * it under the terms of the Affero GNU General Public License as
- * published by the Free Software Foundation, either version 3 of
- * the License, or (at your option) any later version.
+ * http://www.apache.org/licenses/LICENSE-2.0
*
- * Pixels is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * Affero GNU General Public License for more details.
- *
- * You should have received a copy of the Affero GNU General Public
- * License along with Pixels. If not, see
- * .
+ * 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 io.pixelsdb.pixels.sink.provider;
diff --git a/src/main/java/io/pixelsdb/pixels/sink/provider/TableEventEngineProvider.java b/src/main/java/io/pixelsdb/pixels/sink/provider/TableEventEngineProvider.java
deleted file mode 100644
index 2a3f0da..0000000
--- a/src/main/java/io/pixelsdb/pixels/sink/provider/TableEventEngineProvider.java
+++ /dev/null
@@ -1,55 +0,0 @@
-/*
- * Copyright 2025 PixelsDB.
- *
- * This file is part of Pixels.
- *
- * Pixels is free software: you can redistribute it and/or modify
- * it under the terms of the Affero GNU General Public License as
- * published by the Free Software Foundation, either version 3 of
- * the License, or (at your option) any later version.
- *
- * Pixels is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * Affero GNU General Public License for more details.
- *
- * You should have received a copy of the Affero GNU General Public
- * License along with Pixels. If not, see
- * .
- */
-
-package io.pixelsdb.pixels.sink.provider;
-
-
-import io.pixelsdb.pixels.sink.event.RowChangeEvent;
-import io.pixelsdb.pixels.sink.event.deserializer.RowChangeEventStructDeserializer;
-import io.pixelsdb.pixels.sink.exception.SinkException;
-import org.apache.kafka.connect.source.SourceRecord;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-
-/**
- * @package: io.pixelsdb.pixels.sink.provider
- * @className: TableEventEngineProvider
- * @author: AntiO2
- * @date: 2025/9/26 10:45
- */
-public class TableEventEngineProvider extends TableEventProvider
-{
- private final Logger LOGGER = LoggerFactory.getLogger(TableEventEngineProvider.class.getName());
-
- @Override
- RowChangeEvent convertToTargetRecord(T record)
- {
- SourceRecord sourceRecord = (SourceRecord) record;
- try
- {
- return RowChangeEventStructDeserializer.convertToRowChangeEvent(sourceRecord);
- } catch (SinkException e)
- {
- LOGGER.warn("Failed to convert RowChangeEvent to RowChangeEventStruct {}", e.getMessage());
- return null;
- }
- }
-}
diff --git a/src/main/java/io/pixelsdb/pixels/sink/provider/TableEventKafkaProvider.java b/src/main/java/io/pixelsdb/pixels/sink/provider/TableEventKafkaProvider.java
deleted file mode 100644
index b582b2c..0000000
--- a/src/main/java/io/pixelsdb/pixels/sink/provider/TableEventKafkaProvider.java
+++ /dev/null
@@ -1,114 +0,0 @@
-/*
- * Copyright 2025 PixelsDB.
- *
- * This file is part of Pixels.
- *
- * Pixels is free software: you can redistribute it and/or modify
- * it under the terms of the Affero GNU General Public License as
- * published by the Free Software Foundation, either version 3 of
- * the License, or (at your option) any later version.
- *
- * Pixels is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * Affero GNU General Public License for more details.
- *
- * You should have received a copy of the Affero GNU General Public
- * License along with Pixels. If not, see
- * .
- */
-
-package io.pixelsdb.pixels.sink.provider;
-
-import io.pixelsdb.pixels.sink.config.PixelsSinkConfig;
-import io.pixelsdb.pixels.sink.config.factory.PixelsSinkConfigFactory;
-import io.pixelsdb.pixels.sink.event.RowChangeEvent;
-import io.pixelsdb.pixels.sink.util.DataTransform;
-import org.apache.kafka.clients.consumer.ConsumerConfig;
-import org.apache.kafka.clients.consumer.ConsumerRecords;
-import org.apache.kafka.clients.consumer.KafkaConsumer;
-import org.apache.kafka.common.errors.InterruptException;
-import org.apache.kafka.common.errors.WakeupException;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import java.io.IOException;
-import java.time.Duration;
-import java.util.Collections;
-import java.util.Properties;
-import java.util.concurrent.atomic.AtomicBoolean;
-
-public class TableEventKafkaProvider extends TableEventProvider
-{
- private static final Logger log = LoggerFactory.getLogger(TableEventKafkaProvider.class);
- private final Properties kafkaProperties;
- private final String topic;
- private final AtomicBoolean running = new AtomicBoolean(true);
- private final String tableName;
- private KafkaConsumer consumer;
-
- public TableEventKafkaProvider(Properties kafkaProperties, String topic) throws IOException
- {
- PixelsSinkConfig config = PixelsSinkConfigFactory.getInstance();
- this.kafkaProperties = kafkaProperties;
- this.topic = topic;
- this.kafkaProperties.put(ConsumerConfig.GROUP_ID_CONFIG, config.getGroupId() + "-" + topic);
- this.kafkaProperties.put(ConsumerConfig.ALLOW_AUTO_CREATE_TOPICS_CONFIG, "false");
- this.kafkaProperties.put(ConsumerConfig.MAX_POLL_RECORDS_CONFIG, 500);
- this.tableName = DataTransform.extractTableName(topic);
- }
-
- @Override
- protected void processLoop()
- {
- try
- {
- consumer = new KafkaConsumer<>(kafkaProperties);
- consumer.subscribe(Collections.singleton(topic));
-
- while (running.get())
- {
- try
- {
- ConsumerRecords records = consumer.poll(Duration.ofSeconds(5));
- if (!records.isEmpty())
- {
- log.info("{} Consumer poll returned {} records", tableName, records.count());
- records.forEach(record ->
- {
- if (record.value() == null)
- {
- return;
- }
- metricsFacade.recordSerdRowChange();
- putRowChangeEvent(record.value());
- });
- }
- } catch (InterruptException ignored)
- {
- Thread.currentThread().interrupt();
- break;
- }
- }
- } catch (WakeupException e)
- {
- log.info("Consumer wakeup triggered for {}", tableName);
- } catch (Exception e)
- {
- log.info("Exception: {}", e.getMessage());
- } finally
- {
- if (consumer != null)
- {
- consumer.close(Duration.ofSeconds(5));
- log.info("Kafka consumer closed for {}", tableName);
- }
- }
- }
-
- @Override
- RowChangeEvent convertToTargetRecord(Void record)
- {
- throw new UnsupportedOperationException();
- }
-}
diff --git a/src/main/java/io/pixelsdb/pixels/sink/provider/TableEventProvider.java b/src/main/java/io/pixelsdb/pixels/sink/provider/TableEventProvider.java
deleted file mode 100644
index 1dc5111..0000000
--- a/src/main/java/io/pixelsdb/pixels/sink/provider/TableEventProvider.java
+++ /dev/null
@@ -1,54 +0,0 @@
-/*
- * Copyright 2025 PixelsDB.
- *
- * This file is part of Pixels.
- *
- * Pixels is free software: you can redistribute it and/or modify
- * it under the terms of the Affero GNU General Public License as
- * published by the Free Software Foundation, either version 3 of
- * the License, or (at your option) any later version.
- *
- * Pixels is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * Affero GNU General Public License for more details.
- *
- * You should have received a copy of the Affero GNU General Public
- * License along with Pixels. If not, see
- * .
- */
-
-package io.pixelsdb.pixels.sink.provider;
-
-
-import io.pixelsdb.pixels.sink.event.RowChangeEvent;
-
-/**
- * @package: io.pixelsdb.pixels.sink.provider
- * @className: TableEventProvider
- * @author: AntiO2
- * @date: 2025/9/26 07:47
- */
-public abstract class TableEventProvider extends EventProvider
-{
-
- protected void putRowChangeEvent(RowChangeEvent rowChangeEvent)
- {
- putTargetEvent(rowChangeEvent);
- }
-
- public RowChangeEvent getRowChangeEvent()
- {
- return getTargetEvent();
- }
-
- protected void putRawRowChangeEvent(SOURCE_RECORD_T record)
- {
- putRawEvent(record);
- }
-
- final protected void recordSerdEvent()
- {
- metricsFacade.recordSerdRowChange();
- }
-}
diff --git a/src/main/java/io/pixelsdb/pixels/sink/provider/TableEventStorageLoopProvider.java b/src/main/java/io/pixelsdb/pixels/sink/provider/TableEventStorageLoopProvider.java
deleted file mode 100644
index b628876..0000000
--- a/src/main/java/io/pixelsdb/pixels/sink/provider/TableEventStorageLoopProvider.java
+++ /dev/null
@@ -1,90 +0,0 @@
-/*
- * Copyright 2025 PixelsDB.
- *
- * This file is part of Pixels.
- *
- * Pixels is free software: you can redistribute it and/or modify
- * it under the terms of the Affero GNU General Public License as
- * published by the Free Software Foundation, either version 3 of
- * the License, or (at your option) any later version.
- *
- * Pixels is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * Affero GNU General Public License for more details.
- *
- * You should have received a copy of the Affero GNU General Public
- * License along with Pixels. If not, see
- * .
- */
-
-package io.pixelsdb.pixels.sink.provider;
-
-import com.google.protobuf.InvalidProtocolBufferException;
-import io.pixelsdb.pixels.core.utils.Pair;
-import io.pixelsdb.pixels.sink.SinkProto;
-import io.pixelsdb.pixels.sink.config.PixelsSinkConfig;
-import io.pixelsdb.pixels.sink.config.factory.PixelsSinkConfigFactory;
-import io.pixelsdb.pixels.sink.event.RowChangeEvent;
-import io.pixelsdb.pixels.sink.event.deserializer.RowChangeEventStructDeserializer;
-import io.pixelsdb.pixels.sink.exception.SinkException;
-import io.pixelsdb.pixels.sink.util.DataTransform;
-
-import java.nio.ByteBuffer;
-import java.util.logging.Logger;
-
-public class TableEventStorageLoopProvider extends TableEventProvider
-{
- private final Logger LOGGER = Logger.getLogger(TableEventStorageProvider.class.getName());
- private final boolean freshness_embed;
- private final boolean freshness_timestamp;
-
- protected TableEventStorageLoopProvider()
- {
- super();
- PixelsSinkConfig config = PixelsSinkConfigFactory.getInstance();
- String sinkMonitorFreshnessLevel = config.getSinkMonitorFreshnessLevel();
- if (sinkMonitorFreshnessLevel.equals("embed"))
- {
- freshness_embed = true;
- } else
- {
- freshness_embed = false;
- }
- freshness_timestamp = config.isSinkMonitorFreshnessTimestamp();
- }
-
- @Override
- RowChangeEvent convertToTargetRecord(T record)
- {
- Pair pairRecord = (Pair) record;
- ByteBuffer sourceRecord = pairRecord.getLeft();
- sourceRecord.rewind();
- Integer loopId = pairRecord.getRight();
- try
- {
- SinkProto.RowRecord rowRecord = SinkProto.RowRecord.parseFrom(sourceRecord);
-
- SinkProto.RowRecord.Builder rowRecordBuilder = rowRecord.toBuilder();
- if (freshness_timestamp)
- {
- DataTransform.updateRecordTimestamp(rowRecordBuilder, System.currentTimeMillis() * 1000);
- }
-
-// if(rowRecord.getSource().getTable().equals("transfer"))
-// {
-// DataTransform.transIdToBigint(rowRecordBuilder);
-// }
-
- SinkProto.TransactionInfo.Builder transactionBuilder = rowRecordBuilder.getTransactionBuilder();
- String id = transactionBuilder.getId();
- transactionBuilder.setId(id + "_" + loopId);
- rowRecordBuilder.setTransaction(transactionBuilder);
- return RowChangeEventStructDeserializer.convertToRowChangeEvent(rowRecordBuilder.build());
- } catch (InvalidProtocolBufferException | SinkException e)
- {
- LOGGER.warning(e.getMessage());
- return null;
- }
- }
-}
diff --git a/src/main/java/io/pixelsdb/pixels/sink/provider/TableEventStorageProvider.java b/src/main/java/io/pixelsdb/pixels/sink/provider/TableEventStorageProvider.java
deleted file mode 100644
index 8ad49d3..0000000
--- a/src/main/java/io/pixelsdb/pixels/sink/provider/TableEventStorageProvider.java
+++ /dev/null
@@ -1,62 +0,0 @@
-/*
- * Copyright 2025 PixelsDB.
- *
- * This file is part of Pixels.
- *
- * Pixels is free software: you can redistribute it and/or modify
- * it under the terms of the Affero GNU General Public License as
- * published by the Free Software Foundation, either version 3 of
- * the License, or (at your option) any later version.
- *
- * Pixels is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * Affero GNU General Public License for more details.
- *
- * You should have received a copy of the Affero GNU General Public
- * License along with Pixels. If not, see
- * .
- */
-
-package io.pixelsdb.pixels.sink.provider;
-
-
-import com.google.protobuf.InvalidProtocolBufferException;
-import io.pixelsdb.pixels.sink.SinkProto;
-import io.pixelsdb.pixels.sink.event.RowChangeEvent;
-import io.pixelsdb.pixels.sink.event.deserializer.RowChangeEventStructDeserializer;
-import io.pixelsdb.pixels.sink.exception.SinkException;
-
-import java.nio.ByteBuffer;
-import java.util.logging.Logger;
-
-/**
- * @package: io.pixelsdb.pixels.sink.event
- * @className: TableEventStorageProvider
- * @author: AntiO2
- * @date: 2025/9/26 10:45
- */
-public class TableEventStorageProvider extends TableEventProvider
-{
- private final Logger LOGGER = Logger.getLogger(TableEventStorageProvider.class.getName());
-
- protected TableEventStorageProvider()
- {
- super();
- }
-
- @Override
- RowChangeEvent convertToTargetRecord(T record)
- {
- ByteBuffer sourceRecord = (ByteBuffer) record;
- try
- {
- SinkProto.RowRecord rowRecord = SinkProto.RowRecord.parseFrom(sourceRecord);
- return RowChangeEventStructDeserializer.convertToRowChangeEvent(rowRecord);
- } catch (InvalidProtocolBufferException | SinkException e)
- {
- LOGGER.warning(e.getMessage());
- return null;
- }
- }
-}
diff --git a/src/main/java/io/pixelsdb/pixels/sink/provider/TableProviderAndProcessorPipelineManager.java b/src/main/java/io/pixelsdb/pixels/sink/provider/TableProviderAndProcessorPipelineManager.java
deleted file mode 100644
index 128f70b..0000000
--- a/src/main/java/io/pixelsdb/pixels/sink/provider/TableProviderAndProcessorPipelineManager.java
+++ /dev/null
@@ -1,96 +0,0 @@
-/*
- * Copyright 2025 PixelsDB.
- *
- * This file is part of Pixels.
- *
- * Pixels is free software: you can redistribute it and/or modify
- * it under the terms of the Affero GNU General Public License as
- * published by the Free Software Foundation, either version 3 of
- * the License, or (at your option) any later version.
- *
- * Pixels is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * Affero GNU General Public License for more details.
- *
- * You should have received a copy of the Affero GNU General Public
- * License along with Pixels. If not, see
- * .
- */
-
-package io.pixelsdb.pixels.sink.provider;
-
-
-import io.pixelsdb.pixels.common.metadata.SchemaTableName;
-import io.pixelsdb.pixels.core.utils.Pair;
-import io.pixelsdb.pixels.sink.processor.TableProcessor;
-import org.apache.kafka.connect.source.SourceRecord;
-
-import java.nio.ByteBuffer;
-import java.util.Map;
-import java.util.concurrent.ConcurrentHashMap;
-import java.util.concurrent.atomic.AtomicInteger;
-
-/**
- * @package: io.pixelsdb.pixels.sink.provider
- * @className: TableProviderAndProcessorPipelineManager
- * @author: AntiO2
- * @date: 2025/9/26 10:44
- */
-public class TableProviderAndProcessorPipelineManager
-{
- protected final Map activeTableProcessors = new ConcurrentHashMap<>();
- protected final Map tableIds = new ConcurrentHashMap<>();
- private final Map> tableProviders = new ConcurrentHashMap<>();
- private final AtomicInteger nextTableId = new AtomicInteger();
-
-
- public void routeRecord(SchemaTableName schemaTableName, SOURCE_RECORD_T record)
- {
- routeRecord(getTableId(schemaTableName), record);
- }
-
- public void routeRecord(Integer tableId, SOURCE_RECORD_T record)
- {
- TableEventProvider pipeline = tableProviders.computeIfAbsent(tableId, k ->
- {
- TableEventProvider newPipeline = createProvider(record);
- TableProcessor tableProcessor = activeTableProcessors.computeIfAbsent(tableId, k2 ->
- new TableProcessor(newPipeline)
- );
- tableProcessor.run();
- newPipeline.run();
- return newPipeline;
- });
- pipeline.putRawEvent(record);
- }
-
- private TableEventProvider createProvider(SOURCE_RECORD_T record)
- {
- Class> recordType = record.getClass();
- if (recordType == Pair.class)
- {
- return new TableEventStorageLoopProvider<>();
- }
- if (recordType == SourceRecord.class)
- {
- return new TableEventEngineProvider<>();
- } else if (ByteBuffer.class.isAssignableFrom(recordType))
- {
- return new TableEventStorageProvider<>();
- } else
- {
- throw new IllegalArgumentException("Unsupported record type: " + recordType.getName());
- }
- }
-
- private Integer getTableId(SchemaTableName schemaTableName)
- {
- return tableIds.computeIfAbsent(schemaTableName, k -> allocateTableId());
- }
-
- private Integer allocateTableId()
- {
- return nextTableId.getAndIncrement();
- }
-}
diff --git a/src/main/java/io/pixelsdb/pixels/sink/provider/TransactionEventEngineProvider.java b/src/main/java/io/pixelsdb/pixels/sink/provider/TransactionEventEngineProvider.java
deleted file mode 100644
index e39d853..0000000
--- a/src/main/java/io/pixelsdb/pixels/sink/provider/TransactionEventEngineProvider.java
+++ /dev/null
@@ -1,52 +0,0 @@
-/*
- * Copyright 2025 PixelsDB.
- *
- * This file is part of Pixels.
- *
- * Pixels is free software: you can redistribute it and/or modify
- * it under the terms of the Affero GNU General Public License as
- * published by the Free Software Foundation, either version 3 of
- * the License, or (at your option) any later version.
- *
- * Pixels is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * Affero GNU General Public License for more details.
- *
- * You should have received a copy of the Affero GNU General Public
- * License along with Pixels. If not, see
- * .
- */
-
-package io.pixelsdb.pixels.sink.provider;
-
-
-import io.pixelsdb.pixels.sink.SinkProto;
-import io.pixelsdb.pixels.sink.event.deserializer.TransactionStructMessageDeserializer;
-import org.apache.kafka.connect.data.Struct;
-import org.apache.kafka.connect.source.SourceRecord;
-
-/**
- * @package: io.pixelsdb.pixels.sink.provider
- * @className: TransactionEventEngineProvider
- * @author: AntiO2
- * @date: 2025/9/25 13:20
- */
-public class TransactionEventEngineProvider extends TransactionEventProvider
-{
-
- public static final TransactionEventEngineProvider INSTANCE = new TransactionEventEngineProvider<>();
-
- public static TransactionEventEngineProvider getInstance()
- {
- return INSTANCE;
- }
-
- @Override
- SinkProto.TransactionMetadata convertToTargetRecord(T record)
- {
- SourceRecord sourceRecord = (SourceRecord) record;
- Struct value = (Struct) sourceRecord.value();
- return TransactionStructMessageDeserializer.convertToTransactionMetadata(value);
- }
-}
diff --git a/src/main/java/io/pixelsdb/pixels/sink/provider/TransactionEventKafkaProvider.java b/src/main/java/io/pixelsdb/pixels/sink/provider/TransactionEventKafkaProvider.java
deleted file mode 100644
index 081e0cf..0000000
--- a/src/main/java/io/pixelsdb/pixels/sink/provider/TransactionEventKafkaProvider.java
+++ /dev/null
@@ -1,98 +0,0 @@
-/*
- * Copyright 2025 PixelsDB.
- *
- * This file is part of Pixels.
- *
- * Pixels is free software: you can redistribute it and/or modify
- * it under the terms of the Affero GNU General Public License as
- * published by the Free Software Foundation, either version 3 of
- * the License, or (at your option) any later version.
- *
- * Pixels is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * Affero GNU General Public License for more details.
- *
- * You should have received a copy of the Affero GNU General Public
- * License along with Pixels. If not, see
- * .
- */
-
-package io.pixelsdb.pixels.sink.provider;
-
-
-import io.pixelsdb.pixels.sink.SinkProto;
-import io.pixelsdb.pixels.sink.config.PixelsSinkConfig;
-import io.pixelsdb.pixels.sink.config.factory.PixelsSinkConfigFactory;
-import org.apache.kafka.clients.consumer.ConsumerRecord;
-import org.apache.kafka.clients.consumer.ConsumerRecords;
-import org.apache.kafka.clients.consumer.KafkaConsumer;
-import org.apache.kafka.common.errors.WakeupException;
-
-import java.time.Duration;
-import java.util.Collections;
-import java.util.Properties;
-import java.util.concurrent.atomic.AtomicBoolean;
-
-/**
- * @package: io.pixelsdb.pixels.sink.provider
- * @className: TransactionEventKafkaProvider
- * @author: AntiO2
- * @date: 2025/9/25 13:40
- */
-public class TransactionEventKafkaProvider extends TransactionEventProvider
-{
- private final AtomicBoolean running = new AtomicBoolean(true);
- private final String transactionTopic;
- private final KafkaConsumer consumer;
-
- private TransactionEventKafkaProvider()
- {
- Properties kafkaProperties = new Properties();
- PixelsSinkConfig pixelsSinkConfig = PixelsSinkConfigFactory.getInstance();
- this.transactionTopic = pixelsSinkConfig.getTopicPrefix() + "." + pixelsSinkConfig.getTransactionTopicSuffix();
- this.consumer = new KafkaConsumer<>(kafkaProperties);
- }
-
-
- @Override
- public void processLoop()
- {
- consumer.subscribe(Collections.singletonList(transactionTopic));
- while (running.get())
- {
- try
- {
-
- ConsumerRecords records =
- consumer.poll(Duration.ofMillis(1000));
-
- for (ConsumerRecord record : records)
- {
- if (record.value() == null)
- {
- continue;
- }
- putTargetEvent(record.value());
- }
- } catch (WakeupException e)
- {
- if (running.get())
- {
- // LOGGER.warn("Consumer wakeup unexpectedly", e);
- }
- } catch (Exception e)
- {
- e.printStackTrace();
- throw new RuntimeException(e);
- }
- }
- }
-
- @Override
- SinkProto.TransactionMetadata convertToTargetRecord(T record)
- {
- throw new UnsupportedOperationException();
- }
-
-}
diff --git a/src/main/java/io/pixelsdb/pixels/sink/provider/TransactionEventProvider.java b/src/main/java/io/pixelsdb/pixels/sink/provider/TransactionEventProvider.java
deleted file mode 100644
index 9239ba1..0000000
--- a/src/main/java/io/pixelsdb/pixels/sink/provider/TransactionEventProvider.java
+++ /dev/null
@@ -1,41 +0,0 @@
-/*
- * Copyright 2025 PixelsDB.
- *
- * This file is part of Pixels.
- *
- * Pixels is free software: you can redistribute it and/or modify
- * it under the terms of the Affero GNU General Public License as
- * published by the Free Software Foundation, either version 3 of
- * the License, or (at your option) any later version.
- *
- * Pixels is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * Affero GNU General Public License for more details.
- *
- * You should have received a copy of the Affero GNU General Public
- * License along with Pixels. If not, see
- * .
- */
-
-package io.pixelsdb.pixels.sink.provider;
-
-import io.pixelsdb.pixels.sink.SinkProto;
-
-public abstract class TransactionEventProvider extends EventProvider
-{
- public void putTransRawEvent(SOURCE_RECORD_T record)
- {
- putRawEvent(record);
- }
-
- public SinkProto.TransactionMetadata getTransaction()
- {
- return getTargetEvent();
- }
-
- final protected void recordSerdEvent()
- {
- metricsFacade.recordSerdTxChange();
- }
-}
diff --git a/src/main/java/io/pixelsdb/pixels/sink/provider/TransactionEventStorageLoopProvider.java b/src/main/java/io/pixelsdb/pixels/sink/provider/TransactionEventStorageLoopProvider.java
deleted file mode 100644
index 956de39..0000000
--- a/src/main/java/io/pixelsdb/pixels/sink/provider/TransactionEventStorageLoopProvider.java
+++ /dev/null
@@ -1,47 +0,0 @@
-/*
- * Copyright 2025 PixelsDB.
- *
- * This file is part of Pixels.
- *
- * Pixels is free software: you can redistribute it and/or modify
- * it under the terms of the Affero GNU General Public License as
- * published by the Free Software Foundation, either version 3 of
- * the License, or (at your option) any later version.
- *
- * Pixels is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * Affero GNU General Public License for more details.
- *
- * You should have received a copy of the Affero GNU General Public
- * License along with Pixels. If not, see
- * .
- */
-
-package io.pixelsdb.pixels.sink.provider;
-
-import com.google.protobuf.InvalidProtocolBufferException;
-import io.pixelsdb.pixels.core.utils.Pair;
-import io.pixelsdb.pixels.sink.SinkProto;
-
-import java.nio.ByteBuffer;
-
-public class TransactionEventStorageLoopProvider extends TransactionEventProvider
-{
- @Override
- SinkProto.TransactionMetadata convertToTargetRecord(T record)
- {
- Pair buffer = (Pair) record;
- try
- {
- SinkProto.TransactionMetadata tx = SinkProto.TransactionMetadata.parseFrom(buffer.getLeft());
- Integer loopId = buffer.getRight();
- SinkProto.TransactionMetadata.Builder builder = tx.toBuilder();
- builder.setId(builder.getId() + "_" + loopId);
- return builder.build();
- } catch (InvalidProtocolBufferException e)
- {
- throw new RuntimeException(e);
- }
- }
-}
diff --git a/src/main/java/io/pixelsdb/pixels/sink/provider/TransactionEventStorageProvider.java b/src/main/java/io/pixelsdb/pixels/sink/provider/TransactionEventStorageProvider.java
deleted file mode 100644
index 59198c3..0000000
--- a/src/main/java/io/pixelsdb/pixels/sink/provider/TransactionEventStorageProvider.java
+++ /dev/null
@@ -1,43 +0,0 @@
-/*
- * Copyright 2025 PixelsDB.
- *
- * This file is part of Pixels.
- *
- * Pixels is free software: you can redistribute it and/or modify
- * it under the terms of the Affero GNU General Public License as
- * published by the Free Software Foundation, either version 3 of
- * the License, or (at your option) any later version.
- *
- * Pixels is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * Affero GNU General Public License for more details.
- *
- * You should have received a copy of the Affero GNU General Public
- * License along with Pixels. If not, see
- * .
- */
-
-package io.pixelsdb.pixels.sink.provider;
-
-import com.google.protobuf.InvalidProtocolBufferException;
-import io.pixelsdb.pixels.sink.SinkProto;
-
-import java.nio.ByteBuffer;
-
-public class TransactionEventStorageProvider extends TransactionEventProvider
-{
- @Override
- SinkProto.TransactionMetadata convertToTargetRecord(T record)
- {
- ByteBuffer buffer = (ByteBuffer) record;
- try
- {
- SinkProto.TransactionMetadata tx = SinkProto.TransactionMetadata.parseFrom(buffer);
- return tx;
- } catch (InvalidProtocolBufferException e)
- {
- throw new RuntimeException(e);
- }
- }
-}
diff --git a/src/main/java/io/pixelsdb/pixels/sink/source/SinkSource.java b/src/main/java/io/pixelsdb/pixels/sink/source/SinkSource.java
index bf2bb4b..50b1014 100644
--- a/src/main/java/io/pixelsdb/pixels/sink/source/SinkSource.java
+++ b/src/main/java/io/pixelsdb/pixels/sink/source/SinkSource.java
@@ -1,38 +1,40 @@
/*
- * Copyright 2025 PixelsDB.
+ * Copyright 2023 PixelsDB.
*
- * This file is part of Pixels.
+ * Licensed 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
*
- * Pixels is free software: you can redistribute it and/or modify
- * it under the terms of the Affero GNU General Public License as
- * published by the Free Software Foundation, either version 3 of
- * the License, or (at your option) any later version.
+ * http://www.apache.org/licenses/LICENSE-2.0
*
- * Pixels is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * Affero GNU General Public License for more details.
- *
- * You should have received a copy of the Affero GNU General Public
- * License along with Pixels. If not, see
- * .
+ * 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 io.pixelsdb.pixels.sink.source;
-
-import io.pixelsdb.pixels.sink.processor.StoppableProcessor;
-
/**
* @package: io.pixelsdb.pixels.sink.source
* @className: SinkSource
* @author: AntiO2
* @date: 2025/9/26 13:45
*/
-public interface SinkSource extends StoppableProcessor
+public interface SinkSource extends AutoCloseable
{
void start();
boolean isRunning();
+
+ /**
+ * Stops producing new events and waits for pending events to be processed.
+ */
+ @Override
+ void close();
+
+ /**
+ * Stops immediately and discards events that have not been processed.
+ */
+ void abort();
}
diff --git a/src/main/java/io/pixelsdb/pixels/sink/source/SinkSourceFactory.java b/src/main/java/io/pixelsdb/pixels/sink/source/SinkSourceFactory.java
index c0463d0..2f7ca8a 100644
--- a/src/main/java/io/pixelsdb/pixels/sink/source/SinkSourceFactory.java
+++ b/src/main/java/io/pixelsdb/pixels/sink/source/SinkSourceFactory.java
@@ -1,31 +1,28 @@
/*
* Copyright 2025 PixelsDB.
*
- * This file is part of Pixels.
+ * Licensed 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
*
- * Pixels is free software: you can redistribute it and/or modify
- * it under the terms of the Affero GNU General Public License as
- * published by the Free Software Foundation, either version 3 of
- * the License, or (at your option) any later version.
+ * http://www.apache.org/licenses/LICENSE-2.0
*
- * Pixels is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * Affero GNU General Public License for more details.
- *
- * You should have received a copy of the Affero GNU General Public
- * License along with Pixels. If not, see
- * .
+ * 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 io.pixelsdb.pixels.sink.source;
import io.pixelsdb.pixels.sink.config.PixelsSinkConfig;
import io.pixelsdb.pixels.sink.config.factory.PixelsSinkConfigFactory;
import io.pixelsdb.pixels.sink.source.engine.SinkEngineSource;
import io.pixelsdb.pixels.sink.source.kafka.SinkKafkaSource;
-import io.pixelsdb.pixels.sink.source.storage.FasterSinkStorageSource;
+import io.pixelsdb.pixels.sink.source.storage.MemorySinkStorageSource;
+import io.pixelsdb.pixels.sink.source.storage.StreamingSinkStorageSource;
+
+import java.util.Locale;
public class SinkSourceFactory
{
@@ -36,8 +33,20 @@ public static SinkSource createSinkSource()
{
case "kafka" -> new SinkKafkaSource();
case "engine" -> new SinkEngineSource();
- case "storage" -> new FasterSinkStorageSource();
+ case "storage" -> createStorageSource(config);
default -> throw new IllegalStateException("Unsupported data source type: " + config.getDataSource());
};
}
+
+ private static SinkSource createStorageSource(PixelsSinkConfig config)
+ {
+ String storageMode = config.getSinkStorageMode().trim().toLowerCase(Locale.ROOT);
+ return switch (storageMode)
+ {
+ case "stream" -> new StreamingSinkStorageSource();
+ case "memory" -> new MemorySinkStorageSource();
+ default -> throw new IllegalStateException(
+ "Unsupported storage source mode: " + config.getSinkStorageMode());
+ };
+ }
}
diff --git a/src/main/java/io/pixelsdb/pixels/sink/source/engine/ConnectEventClassifier.java b/src/main/java/io/pixelsdb/pixels/sink/source/engine/ConnectEventClassifier.java
new file mode 100644
index 0000000..ad53b97
--- /dev/null
+++ b/src/main/java/io/pixelsdb/pixels/sink/source/engine/ConnectEventClassifier.java
@@ -0,0 +1,105 @@
+/*
+ * Copyright 2026 PixelsDB.
+ *
+ * Licensed 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 io.pixelsdb.pixels.sink.source.engine;
+
+import io.pixelsdb.pixels.common.metadata.SchemaTableName;
+import io.pixelsdb.pixels.sink.conversion.debezium.support.DebeziumRecordUtil;
+import org.apache.kafka.connect.data.Struct;
+import org.apache.kafka.connect.source.SourceRecord;
+
+import java.util.Locale;
+
+/**
+ * Classifies Kafka Connect {@link SourceRecord} Debezium envelopes.
+ */
+public final class ConnectEventClassifier implements DebeziumEventClassifier
+{
+ @Override
+ public DebeziumRecordType classify(
+ SourceRecord sourceRecord, String transactionTopic)
+ {
+ if (sourceRecord == null || sourceRecord.value() == null)
+ {
+ return DebeziumRecordType.TOMBSTONE;
+ }
+ if (!(sourceRecord.value() instanceof Struct value))
+ {
+ return DebeziumRecordType.UNKNOWN_CONTROL;
+ }
+
+ if (transactionTopic != null && transactionTopic.equals(sourceRecord.topic()))
+ {
+ String status = DebeziumRecordUtil.getStringSafely(value, "status");
+ String id = DebeziumRecordUtil.getStringSafely(value, "id");
+ return (status.equals("BEGIN") || status.equals("END")) && !id.isBlank()
+ ? DebeziumRecordType.TRANSACTION
+ : DebeziumRecordType.UNKNOWN_CONTROL;
+ }
+
+ return isRowChange(value)
+ ? DebeziumRecordType.ROW
+ : DebeziumRecordType.UNKNOWN_CONTROL;
+ }
+
+ @Override
+ public SchemaTableName tableOf(SourceRecord record)
+ {
+ if (!(record.value() instanceof Struct value))
+ {
+ throw new IllegalArgumentException("Debezium row value must be a Struct");
+ }
+ Object sourceObject = DebeziumRecordUtil.getFieldSafely(value, "source");
+ if (!(sourceObject instanceof Struct source))
+ {
+ throw new IllegalArgumentException("Debezium row source must be a Struct");
+ }
+ String database = DebeziumRecordUtil.getStringSafely(source, "db");
+ String table = DebeziumRecordUtil.getStringSafely(source, "table");
+ if (database.isBlank() || table.isBlank())
+ {
+ throw new IllegalArgumentException(
+ "Debezium row source is missing db or table");
+ }
+ return new SchemaTableName(database, table);
+ }
+
+ private static boolean isRowChange(Struct value)
+ {
+ String op = DebeziumRecordUtil.getStringSafely(value, "op").toLowerCase(Locale.ROOT);
+ if (!(op.equals("c") || op.equals("u") || op.equals("d") || op.equals("r")))
+ {
+ return false;
+ }
+
+ Object sourceObject = DebeziumRecordUtil.getFieldSafely(value, "source");
+ if (!(sourceObject instanceof Struct source) ||
+ DebeziumRecordUtil.getStringSafely(source, "db").isBlank() ||
+ DebeziumRecordUtil.getStringSafely(source, "table").isBlank())
+ {
+ return false;
+ }
+
+ Object before = DebeziumRecordUtil.getFieldSafely(value, "before");
+ Object after = DebeziumRecordUtil.getFieldSafely(value, "after");
+ return switch (op)
+ {
+ case "c", "r" -> after instanceof Struct;
+ case "u" -> before instanceof Struct && after instanceof Struct;
+ case "d" -> before instanceof Struct;
+ default -> false;
+ };
+ }
+}
diff --git a/src/test/java/io/pixelsdb/pixels/sink/TestUtils.java b/src/main/java/io/pixelsdb/pixels/sink/source/engine/DebeziumEventClassifier.java
similarity index 56%
rename from src/test/java/io/pixelsdb/pixels/sink/TestUtils.java
rename to src/main/java/io/pixelsdb/pixels/sink/source/engine/DebeziumEventClassifier.java
index 7713401..dab32a0 100644
--- a/src/test/java/io/pixelsdb/pixels/sink/TestUtils.java
+++ b/src/main/java/io/pixelsdb/pixels/sink/source/engine/DebeziumEventClassifier.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2025 PixelsDB.
+ * Copyright 2023 PixelsDB.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -12,23 +12,18 @@
* 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 io.pixelsdb.pixels.sink.source.engine;
-package io.pixelsdb.pixels.sink;
-
-import java.util.concurrent.ExecutorService;
-import java.util.concurrent.Executors;
+import io.pixelsdb.pixels.common.metadata.SchemaTableName;
-public class TestUtils
+/**
+ * Engine-side classification of Debezium transport records.
+ * Lives in {@code source.engine}, not conversion.
+ */
+public interface DebeziumEventClassifier
{
- public static ExecutorService synchronousExecutor()
- {
- return Executors.newSingleThreadExecutor(runnable ->
- {
- Thread thread = new Thread(runnable);
- thread.setDaemon(true);
- return thread;
- });
- }
+ DebeziumRecordType classify(I input, String transactionTopic);
+
+ SchemaTableName tableOf(I input);
}
diff --git a/src/main/java/io/pixelsdb/pixels/sink/source/engine/DebeziumRecordType.java b/src/main/java/io/pixelsdb/pixels/sink/source/engine/DebeziumRecordType.java
new file mode 100644
index 0000000..d90cb8f
--- /dev/null
+++ b/src/main/java/io/pixelsdb/pixels/sink/source/engine/DebeziumRecordType.java
@@ -0,0 +1,27 @@
+/*
+ * Copyright 2023 PixelsDB.
+ *
+ * Licensed 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 io.pixelsdb.pixels.sink.source.engine;
+
+/**
+ * Classification of Debezium Engine {@code SourceRecord} envelopes.
+ */
+public enum DebeziumRecordType
+{
+ ROW,
+ TRANSACTION,
+ TOMBSTONE,
+ UNKNOWN_CONTROL
+}
diff --git a/src/main/java/io/pixelsdb/pixels/sink/source/engine/PixelsDebeziumConsumer.java b/src/main/java/io/pixelsdb/pixels/sink/source/engine/PixelsDebeziumConsumer.java
index 96785a7..2c74e33 100644
--- a/src/main/java/io/pixelsdb/pixels/sink/source/engine/PixelsDebeziumConsumer.java
+++ b/src/main/java/io/pixelsdb/pixels/sink/source/engine/PixelsDebeziumConsumer.java
@@ -1,40 +1,48 @@
/*
* Copyright 2025 PixelsDB.
*
- * This file is part of Pixels.
+ * Licensed 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
*
- * Pixels is free software: you can redistribute it and/or modify
- * it under the terms of the Affero GNU General Public License as
- * published by the Free Software Foundation, either version 3 of
- * the License, or (at your option) any later version.
+ * http://www.apache.org/licenses/LICENSE-2.0
*
- * Pixels is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * Affero GNU General Public License for more details.
- *
- * You should have received a copy of the Affero GNU General Public
- * License along with Pixels. If not, see
- * .
+ * 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 io.pixelsdb.pixels.sink.source.engine;
import io.debezium.engine.DebeziumEngine;
import io.debezium.engine.RecordChangeEvent;
-import io.pixelsdb.pixels.common.metadata.SchemaTableName;
+import io.pixelsdb.pixels.sink.SinkProto;
import io.pixelsdb.pixels.sink.config.PixelsSinkConfig;
import io.pixelsdb.pixels.sink.config.factory.PixelsSinkConfigFactory;
-import io.pixelsdb.pixels.sink.processor.StoppableProcessor;
-import io.pixelsdb.pixels.sink.processor.TransactionProcessor;
-import io.pixelsdb.pixels.sink.provider.TableProviderAndProcessorPipelineManager;
-import io.pixelsdb.pixels.sink.provider.TransactionEventEngineProvider;
+import io.pixelsdb.pixels.sink.conversion.debezium.DebeziumRowConverter;
+import io.pixelsdb.pixels.sink.conversion.debezium.DebeziumTransactionConverter;
+import io.pixelsdb.pixels.sink.conversion.debezium.connect.DebeziumConnectRowConverter;
+import io.pixelsdb.pixels.sink.conversion.debezium.connect.DebeziumConnectTransactionConverter;
+import io.pixelsdb.pixels.sink.conversion.debezium.dialect.DebeziumSourceAdapter;
+import io.pixelsdb.pixels.sink.conversion.debezium.support.DebeziumRecordUtil;
+import io.pixelsdb.pixels.sink.event.RowChangeEvent;
+import io.pixelsdb.pixels.sink.metadata.TableMetadataRegistry;
+import io.pixelsdb.pixels.sink.pipeline.TablePipelineManager;
+import io.pixelsdb.pixels.sink.pipeline.TransactionPipeline;
+import io.pixelsdb.pixels.sink.source.engine.adapter.DebeziumSourceAdapterSelector;
import io.pixelsdb.pixels.sink.util.MetricsFacade;
+import io.pixelsdb.pixels.sink.util.concurrent.StreamOrderedDecoder;
import org.apache.kafka.connect.data.Struct;
import org.apache.kafka.connect.source.SourceRecord;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import java.util.ArrayList;
import java.util.List;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ExecutionException;
/**
* @package: io.pixelsdb.pixels.source
@@ -42,88 +50,209 @@
* @author: AntiO2
* @date: 2025/9/25 12:51
*/
-public class PixelsDebeziumConsumer implements DebeziumEngine.ChangeConsumer>, StoppableProcessor
+public class PixelsDebeziumConsumer
+ implements DebeziumEngine.ChangeConsumer>, AutoCloseable
{
+ private static final Logger LOGGER = LoggerFactory.getLogger(PixelsDebeziumConsumer.class);
+ private static final Object TRANSACTION_STREAM_KEY = new Object();
+ private static final ConnectEventClassifier CLASSIFIER = new ConnectEventClassifier();
+
private final String checkTransactionTopic;
- private final TransactionEventEngineProvider transactionEventProvider = TransactionEventEngineProvider.INSTANCE;
- private final TableProviderAndProcessorPipelineManager tableProvidersManagerImpl = new TableProviderAndProcessorPipelineManager<>();
- private final TransactionProcessor processor = new TransactionProcessor(transactionEventProvider);
- private final Thread transactionProviderThread;
- private final Thread transactionProcessorThread;
+ private final DebeziumSourceAdapter connectorAdapter;
+ private final DebeziumRowConverter rowConverter;
+ private final DebeziumTransactionConverter transactionConverter;
+ private final TransactionPipeline transactionPipeline;
+ private final TablePipelineManager tablePipelineManager;
+ private final StreamOrderedDecoder decodePipeline;
private final MetricsFacade metricsFacade = MetricsFacade.getInstance();
- PixelsSinkConfig pixelsSinkConfig = PixelsSinkConfigFactory.getInstance();
+ private final PixelsSinkConfig pixelsSinkConfig = PixelsSinkConfigFactory.getInstance();
+
+ private record PendingRecord(
+ RecordChangeEvent record,
+ CompletableFuture completion)
+ {
+ }
public PixelsDebeziumConsumer()
{
this.checkTransactionTopic = pixelsSinkConfig.getDebeziumTopicPrefix() + ".transaction";
- this.transactionProviderThread = new Thread(this.transactionEventProvider, "transaction-adapter");
- this.transactionProcessorThread = new Thread(this.processor, "transaction-processor");
+ this.connectorAdapter = DebeziumSourceAdapterSelector.configured();
+ this.rowConverter = new DebeziumConnectRowConverter(
+ TableMetadataRegistry.Instance(), connectorAdapter);
+ this.transactionConverter = new DebeziumConnectTransactionConverter(connectorAdapter);
+ this.transactionPipeline = new TransactionPipeline();
+ this.tablePipelineManager = new TablePipelineManager();
+ this.decodePipeline = new StreamOrderedDecoder(
+ pixelsSinkConfig.getSourceDecodeThreads(),
+ "debezium-decoder");
+ }
- this.transactionProcessorThread.start();
- this.transactionProviderThread.start();
+ public void start()
+ {
+ transactionPipeline.start();
+ decodePipeline.start();
}
public void handleBatch(List> event,
DebeziumEngine.RecordCommitter> committer) throws InterruptedException
{
+ List pendingRecords = new ArrayList<>(event.size());
for (RecordChangeEvent record : event)
{
- try
+ SourceRecord sourceRecord = record.record();
+ if (sourceRecord == null)
{
- SourceRecord sourceRecord = record.record();
- if (sourceRecord == null)
- {
- continue;
- }
+ pendingRecords.add(new PendingRecord(
+ record, CompletableFuture.completedFuture(null)));
+ continue;
+ }
- metricsFacade.recordDebeziumEvent();
- if (isTransactionEvent(sourceRecord))
- {
- handleTransactionSourceRecord(sourceRecord);
- } else
+ metricsFacade.recordDebeziumEvent();
+ DebeziumRecordType recordType =
+ CLASSIFIER.classify(sourceRecord, checkTransactionTopic);
+ logSourceRecord(sourceRecord, recordType);
+ CompletableFuture completion = switch (recordType)
+ {
+ case ROW -> submitRow(sourceRecord);
+ case TRANSACTION -> submitTransaction(sourceRecord);
+ case TOMBSTONE, UNKNOWN_CONTROL ->
{
- handleRowChangeSourceRecord(sourceRecord);
+ LOGGER.debug("Skipping Debezium {} event from topic {}",
+ recordType, sourceRecord.topic());
+ yield CompletableFuture.completedFuture(null);
}
- } finally
- {
- committer.markProcessed(record);
- }
+ };
+ pendingRecords.add(new PendingRecord(record, completion));
+ }
+ for (PendingRecord pending : pendingRecords)
+ {
+ awaitCompletion(pending.completion());
+ committer.markProcessed(pending.record());
}
committer.markBatchFinished();
}
- private void handleTransactionSourceRecord(SourceRecord sourceRecord) throws InterruptedException
+ private CompletableFuture submitRow(SourceRecord record)
+ {
+ return decodePipeline.submit(
+ CLASSIFIER.tableOf(record),
+ record,
+ rowConverter::convert,
+ result -> publishRow(record, result));
+ }
+
+ private CompletableFuture submitTransaction(SourceRecord record)
{
- transactionEventProvider.putTransRawEvent(sourceRecord);
+ return decodePipeline.submit(
+ TRANSACTION_STREAM_KEY,
+ record,
+ transactionConverter::convert,
+ result -> publishTransaction(record, result));
}
- private void handleRowChangeSourceRecord(SourceRecord sourceRecord)
+ private void publishRow(
+ SourceRecord record,
+ StreamOrderedDecoder.DecodeResult result)
{
- Struct value = (Struct) sourceRecord.value();
- if (value == null)
+ if (result.failure() != null)
+ {
+ LOGGER.warn("Skipping invalid Debezium ROW event from topic {}",
+ record.topic(), result.failure());
+ return;
+ }
+ if (result.value() == null)
{
- return; // Delete Record, We will handle it in next record
+ return;
}
- Object sourceObject = value.get("source");
- Struct source = (Struct) sourceObject;
- String schemaName = source.get("db").toString();
- String tableName = source.get("table").toString();
- SchemaTableName schemaTableName = new SchemaTableName(schemaName, tableName);
- tableProvidersManagerImpl.routeRecord(schemaTableName, sourceRecord);
+ metricsFacade.recordSerdRowChange();
+ tablePipelineManager.route(result.value());
}
- private boolean isTransactionEvent(SourceRecord sourceRecord)
+ private void publishTransaction(
+ SourceRecord record,
+ StreamOrderedDecoder.DecodeResult result)
{
- return checkTransactionTopic.equals(sourceRecord.topic());
+ if (result.failure() != null)
+ {
+ LOGGER.warn("Skipping invalid Debezium TRANSACTION event from topic {}",
+ record.topic(), result.failure());
+ return;
+ }
+ if (result.value() == null)
+ {
+ return;
+ }
+ metricsFacade.recordSerdTxChange();
+ transactionPipeline.publish(result.value());
+ }
+
+ private void awaitCompletion(CompletableFuture completion)
+ throws InterruptedException
+ {
+ try
+ {
+ completion.get();
+ } catch (ExecutionException e)
+ {
+ throw new IllegalStateException(
+ "Debezium decode pipeline failed before publishing an event",
+ e.getCause());
+ }
+ }
+
+ private void logSourceRecord(SourceRecord record, DebeziumRecordType recordType)
+ {
+ if (!LOGGER.isDebugEnabled())
+ {
+ return;
+ }
+ Struct value = record.value() instanceof Struct struct ? struct : null;
+ Struct source = value == null ? null :
+ asStruct(DebeziumRecordUtil.getFieldSafely(value, "source"));
+ Struct transaction = value == null ? null :
+ asStruct(DebeziumRecordUtil.getFieldSafely(value, "transaction"));
+ String rawTransactionId = recordType == DebeziumRecordType.TRANSACTION
+ ? DebeziumRecordUtil.getStringSafely(value, "id")
+ : DebeziumRecordUtil.getStringSafely(transaction, "id");
+ String canonicalTransactionId =
+ connectorAdapter.normalizeTransactionId(rawTransactionId);
+
+ LOGGER.debug("Debezium SourceRecord topic={}, sourcePartition={}, sourceOffset={}, " +
+ "keySchema={}, key={}, valueSchema={}, category={}, transaction.id={}, " +
+ "source.gtid={}, source.file={}, source.pos={}, source.row={}",
+ record.topic(), record.sourcePartition(), record.sourceOffset(),
+ schemaName(record.keySchema()), record.key(), schemaName(record.valueSchema()),
+ recordType, canonicalTransactionId,
+ DebeziumRecordUtil.getStringSafely(source, "gtid"),
+ DebeziumRecordUtil.getStringSafely(source, "file"),
+ DebeziumRecordUtil.getStringSafely(source, "pos"),
+ DebeziumRecordUtil.getStringSafely(source, "row"));
+ }
+
+ private static Struct asStruct(Object value)
+ {
+ return value instanceof Struct struct ? struct : null;
+ }
+
+ private static String schemaName(org.apache.kafka.connect.data.Schema schema)
+ {
+ return schema == null ? "" : String.valueOf(schema.name());
}
@Override
- public void stopProcessor()
+ public void close()
+ {
+ decodePipeline.close();
+ tablePipelineManager.close();
+ transactionPipeline.close();
+ }
+
+ public void abort()
{
- transactionProviderThread.interrupt();
- processor.stopProcessor();
- transactionProcessorThread.interrupt();
+ decodePipeline.abort();
+ tablePipelineManager.abort();
+ transactionPipeline.abort();
}
}
diff --git a/src/main/java/io/pixelsdb/pixels/sink/source/engine/SinkEngineSource.java b/src/main/java/io/pixelsdb/pixels/sink/source/engine/SinkEngineSource.java
index 47d2735..08f93f1 100644
--- a/src/main/java/io/pixelsdb/pixels/sink/source/engine/SinkEngineSource.java
+++ b/src/main/java/io/pixelsdb/pixels/sink/source/engine/SinkEngineSource.java
@@ -1,29 +1,25 @@
/*
* Copyright 2025 PixelsDB.
*
- * This file is part of Pixels.
+ * Licensed 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
*
- * Pixels is free software: you can redistribute it and/or modify
- * it under the terms of the Affero GNU General Public License as
- * published by the Free Software Foundation, either version 3 of
- * the License, or (at your option) any later version.
+ * http://www.apache.org/licenses/LICENSE-2.0
*
- * Pixels is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * Affero GNU General Public License for more details.
- *
- * You should have received a copy of the Affero GNU General Public
- * License along with Pixels. If not, see
- * .
+ * 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 io.pixelsdb.pixels.sink.source.engine;
import io.debezium.embedded.Connect;
import io.debezium.engine.DebeziumEngine;
import io.debezium.engine.RecordChangeEvent;
import io.debezium.engine.format.ChangeEventFormat;
+import io.pixelsdb.pixels.sink.config.PixelsSinkConfig;
import io.pixelsdb.pixels.sink.config.factory.PixelsSinkConfigFactory;
import io.pixelsdb.pixels.sink.source.SinkSource;
import org.apache.kafka.connect.source.SourceRecord;
@@ -31,23 +27,29 @@
import java.util.Properties;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
+import java.util.concurrent.TimeUnit;
public class SinkEngineSource implements SinkSource
{
+ private static final long SHUTDOWN_TIMEOUT_SECONDS = 10;
+
+ private final PixelsSinkConfig pixelsSinkConfig;
private final PixelsDebeziumConsumer consumer;
private DebeziumEngine> engine;
private ExecutorService executor;
- private volatile boolean running = true;
+ private volatile boolean running;
public SinkEngineSource()
{
+ this.pixelsSinkConfig = PixelsSinkConfigFactory.getInstance();
this.consumer = new PixelsDebeziumConsumer();
}
public void start()
{
- Properties debeziumProps = PixelsSinkConfigFactory.getInstance()
- .getConfig().extractPropertiesByPrefix("debezium.", true);
+ consumer.start();
+ Properties debeziumProps = pixelsSinkConfig.getConfig()
+ .extractPropertiesByPrefix("debezium.", true);
this.engine = DebeziumEngine.create(ChangeEventFormat.of(Connect.class))
.using(debeziumProps)
@@ -56,10 +58,11 @@ public void start()
this.executor = Executors.newSingleThreadExecutor();
this.executor.execute(engine);
+ running = true;
}
@Override
- public void stopProcessor()
+ public void close()
{
try
{
@@ -70,10 +73,30 @@ public void stopProcessor()
if (executor != null)
{
executor.shutdown();
+ if (!executor.awaitTermination(
+ SHUTDOWN_TIMEOUT_SECONDS, TimeUnit.SECONDS))
+ {
+ executor.shutdownNow();
+ consumer.abort();
+ return;
+ }
+ }
+ consumer.close();
+ } catch (InterruptedException e)
+ {
+ if (executor != null)
+ {
+ executor.shutdownNow();
}
- consumer.stopProcessor();
+ consumer.abort();
+ Thread.currentThread().interrupt();
} catch (Exception e)
{
+ if (executor != null)
+ {
+ executor.shutdownNow();
+ }
+ consumer.abort();
throw new RuntimeException("Failed to stop PixelsSinkEngine", e);
} finally
{
@@ -81,6 +104,29 @@ public void stopProcessor()
}
}
+ @Override
+ public void abort()
+ {
+ running = false;
+ if (executor != null)
+ {
+ executor.shutdownNow();
+ }
+ try
+ {
+ if (engine != null)
+ {
+ engine.close();
+ }
+ } catch (Exception e)
+ {
+ throw new RuntimeException("Failed to abort PixelsSinkEngine", e);
+ } finally
+ {
+ consumer.abort();
+ }
+ }
+
@Override
public boolean isRunning()
{
diff --git a/src/main/java/io/pixelsdb/pixels/sink/source/engine/adapter/DebeziumSourceAdapterSelector.java b/src/main/java/io/pixelsdb/pixels/sink/source/engine/adapter/DebeziumSourceAdapterSelector.java
new file mode 100644
index 0000000..2ac349f
--- /dev/null
+++ b/src/main/java/io/pixelsdb/pixels/sink/source/engine/adapter/DebeziumSourceAdapterSelector.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright 2026 PixelsDB.
+ *
+ * Licensed 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 io.pixelsdb.pixels.sink.source.engine.adapter;
+
+import io.pixelsdb.pixels.sink.config.factory.PixelsSinkConfigFactory;
+import io.pixelsdb.pixels.sink.conversion.debezium.dialect.DebeziumSourceAdapter;
+import io.pixelsdb.pixels.sink.conversion.debezium.dialect.DebeziumSourceAdapterRegistry;
+
+/**
+ * Wiring-side selector for sink CDC dialect ({@code sink.debezium.dialect},
+ * with fallback to {@code debezium.connector.class}).
+ */
+public final class DebeziumSourceAdapterSelector
+{
+ private DebeziumSourceAdapterSelector()
+ {
+ }
+
+ public static DebeziumSourceAdapter configured()
+ {
+ return DebeziumSourceAdapterRegistry.resolve(
+ PixelsSinkConfigFactory.getInstance().resolveDebeziumSourceDialect());
+ }
+
+ public static DebeziumSourceAdapter configuredIfPresent()
+ {
+ String dialect =
+ PixelsSinkConfigFactory.getInstance().resolveDebeziumSourceDialect();
+ return dialect == null || dialect.isBlank()
+ ? null
+ : DebeziumSourceAdapterRegistry.resolve(dialect);
+ }
+}
diff --git a/src/main/java/io/pixelsdb/pixels/sink/source/kafka/KafkaRowSource.java b/src/main/java/io/pixelsdb/pixels/sink/source/kafka/KafkaRowSource.java
new file mode 100644
index 0000000..49fc886
--- /dev/null
+++ b/src/main/java/io/pixelsdb/pixels/sink/source/kafka/KafkaRowSource.java
@@ -0,0 +1,135 @@
+/*
+ * Copyright 2026 PixelsDB.
+ *
+ * Licensed 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 io.pixelsdb.pixels.sink.source.kafka;
+
+import io.pixelsdb.pixels.sink.config.PixelsSinkConfig;
+import io.pixelsdb.pixels.sink.config.factory.PixelsSinkConfigFactory;
+import io.pixelsdb.pixels.sink.event.RowChangeEvent;
+import io.pixelsdb.pixels.sink.pipeline.TablePipelineManager;
+import io.pixelsdb.pixels.sink.source.kafka.serde.KafkaRecordConverter;
+import io.pixelsdb.pixels.sink.util.DataTransform;
+import io.pixelsdb.pixels.sink.util.MetricsFacade;
+import org.apache.kafka.clients.consumer.ConsumerConfig;
+import org.apache.kafka.clients.consumer.ConsumerRecords;
+import org.apache.kafka.clients.consumer.KafkaConsumer;
+import org.apache.kafka.common.errors.InterruptException;
+import org.apache.kafka.common.errors.WakeupException;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.time.Duration;
+import java.util.Collections;
+import java.util.Properties;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+public final class KafkaRowSource implements Runnable
+{
+ private static final Logger LOGGER = LoggerFactory.getLogger(KafkaRowSource.class);
+
+ private final Properties kafkaProperties;
+ private final String topic;
+ private final String tableName;
+ private final TablePipelineManager tablePipelineManager;
+ private final KafkaRecordConverter converter;
+ private final MetricsFacade metricsFacade = MetricsFacade.getInstance();
+ private final AtomicBoolean running = new AtomicBoolean(true);
+ private KafkaConsumer consumer;
+
+ public KafkaRowSource(
+ Properties kafkaProperties,
+ String topic,
+ TablePipelineManager tablePipelineManager)
+ {
+ PixelsSinkConfig config = PixelsSinkConfigFactory.getInstance();
+ this.kafkaProperties = new Properties();
+ this.kafkaProperties.putAll(kafkaProperties);
+ this.kafkaProperties.put(
+ ConsumerConfig.GROUP_ID_CONFIG, config.getGroupId() + "-" + topic);
+ this.kafkaProperties.put(ConsumerConfig.ALLOW_AUTO_CREATE_TOPICS_CONFIG, "false");
+ this.kafkaProperties.put(ConsumerConfig.MAX_POLL_RECORDS_CONFIG, 500);
+ this.topic = topic;
+ this.tableName = DataTransform.extractTableName(topic);
+ this.tablePipelineManager = tablePipelineManager;
+ this.converter = KafkaRecordConverter.forRow(this.kafkaProperties);
+ }
+
+ @Override
+ public void run()
+ {
+ try
+ {
+ consumer = new KafkaConsumer<>(kafkaProperties);
+ consumer.subscribe(Collections.singleton(topic));
+ while (running.get())
+ {
+ try
+ {
+ ConsumerRecords records =
+ consumer.poll(Duration.ofSeconds(5));
+ records.forEach(record ->
+ {
+ byte[] value = record.value();
+ if (value == null)
+ {
+ return;
+ }
+ metricsFacade.addRawData(value.length);
+ try
+ {
+ RowChangeEvent event = converter.convert(topic, value);
+ if (event != null)
+ {
+ metricsFacade.recordSerdRowChange();
+ tablePipelineManager.route(event);
+ }
+ } catch (RuntimeException e)
+ {
+ LOGGER.warn(
+ "Failed to convert Kafka record from topic {}: {}",
+ topic, e.getMessage());
+ }
+ });
+ } catch (InterruptException e)
+ {
+ Thread.currentThread().interrupt();
+ break;
+ }
+ }
+ } catch (WakeupException e)
+ {
+ LOGGER.debug("Consumer wakeup triggered for {}", tableName);
+ } catch (Exception e)
+ {
+ LOGGER.error("Kafka source failed for {}", tableName, e);
+ } finally
+ {
+ converter.close();
+ if (consumer != null)
+ {
+ consumer.close(Duration.ofSeconds(5));
+ }
+ }
+ }
+
+ void requestStop()
+ {
+ running.set(false);
+ if (consumer != null)
+ {
+ consumer.wakeup();
+ }
+ }
+}
diff --git a/src/main/java/io/pixelsdb/pixels/sink/source/kafka/KafkaTransactionSource.java b/src/main/java/io/pixelsdb/pixels/sink/source/kafka/KafkaTransactionSource.java
new file mode 100644
index 0000000..5624d12
--- /dev/null
+++ b/src/main/java/io/pixelsdb/pixels/sink/source/kafka/KafkaTransactionSource.java
@@ -0,0 +1,112 @@
+/*
+ * Copyright 2026 PixelsDB.
+ *
+ * Licensed 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 io.pixelsdb.pixels.sink.source.kafka;
+
+import io.pixelsdb.pixels.sink.SinkProto;
+import io.pixelsdb.pixels.sink.pipeline.TransactionPipeline;
+import io.pixelsdb.pixels.sink.source.kafka.serde.KafkaRecordConverter;
+import io.pixelsdb.pixels.sink.util.MetricsFacade;
+import org.apache.kafka.clients.consumer.ConsumerRecord;
+import org.apache.kafka.clients.consumer.ConsumerRecords;
+import org.apache.kafka.clients.consumer.KafkaConsumer;
+import org.apache.kafka.common.errors.WakeupException;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.time.Duration;
+import java.util.Collections;
+import java.util.Properties;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+public final class KafkaTransactionSource implements Runnable
+{
+ private static final Logger LOGGER = LoggerFactory.getLogger(KafkaTransactionSource.class);
+
+ private final String transactionTopic;
+ private final KafkaConsumer consumer;
+ private final KafkaRecordConverter converter;
+ private final TransactionPipeline transactionPipeline;
+ private final MetricsFacade metricsFacade = MetricsFacade.getInstance();
+ private final AtomicBoolean running = new AtomicBoolean(true);
+
+ public KafkaTransactionSource(
+ Properties kafkaProperties,
+ String transactionTopic,
+ TransactionPipeline transactionPipeline)
+ {
+ this.transactionTopic = transactionTopic;
+ Properties consumerProperties = new Properties();
+ consumerProperties.putAll(kafkaProperties);
+ this.converter = KafkaRecordConverter.forTransaction(consumerProperties);
+ this.consumer = new KafkaConsumer<>(consumerProperties);
+ this.transactionPipeline = transactionPipeline;
+ }
+
+ @Override
+ public void run()
+ {
+ try
+ {
+ consumer.subscribe(Collections.singletonList(transactionTopic));
+ while (running.get())
+ {
+ try
+ {
+ ConsumerRecords records =
+ consumer.poll(Duration.ofMillis(1000));
+ for (ConsumerRecord record : records)
+ {
+ byte[] value = record.value();
+ if (value == null)
+ {
+ continue;
+ }
+ metricsFacade.addRawData(value.length);
+ SinkProto.TransactionMetadata transaction =
+ converter.convert(transactionTopic, value);
+ if (transaction != null)
+ {
+ metricsFacade.recordSerdTxChange();
+ transactionPipeline.publish(transaction);
+ }
+ }
+ } catch (WakeupException e)
+ {
+ if (running.get())
+ {
+ throw e;
+ }
+ }
+ }
+ } catch (Exception e)
+ {
+ if (running.get())
+ {
+ LOGGER.error("Kafka transaction source failed for {}", transactionTopic, e);
+ }
+ } finally
+ {
+ converter.close();
+ consumer.close(Duration.ofSeconds(5));
+ }
+ }
+
+ void requestStop()
+ {
+ running.set(false);
+ consumer.wakeup();
+ }
+}
diff --git a/src/main/java/io/pixelsdb/pixels/sink/source/kafka/SinkKafkaSource.java b/src/main/java/io/pixelsdb/pixels/sink/source/kafka/SinkKafkaSource.java
index 423e1d1..3e661b5 100644
--- a/src/main/java/io/pixelsdb/pixels/sink/source/kafka/SinkKafkaSource.java
+++ b/src/main/java/io/pixelsdb/pixels/sink/source/kafka/SinkKafkaSource.java
@@ -1,40 +1,43 @@
/*
* Copyright 2025 PixelsDB.
*
- * This file is part of Pixels.
+ * Licensed 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
*
- * Pixels is free software: you can redistribute it and/or modify
- * it under the terms of the Affero GNU General Public License as
- * published by the Free Software Foundation, either version 3 of
- * the License, or (at your option) any later version.
+ * http://www.apache.org/licenses/LICENSE-2.0
*
- * Pixels is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * Affero GNU General Public License for more details.
- *
- * You should have received a copy of the Affero GNU General Public
- * License along with Pixels. If not, see
- * .
+ * 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 io.pixelsdb.pixels.sink.source.kafka;
import io.pixelsdb.pixels.sink.config.PixelsSinkConfig;
import io.pixelsdb.pixels.sink.config.PixelsSinkConstants;
import io.pixelsdb.pixels.sink.config.factory.KafkaPropFactorySelector;
import io.pixelsdb.pixels.sink.config.factory.PixelsSinkConfigFactory;
-import io.pixelsdb.pixels.sink.processor.MonitorThreadManager;
-import io.pixelsdb.pixels.sink.processor.TopicProcessor;
-import io.pixelsdb.pixels.sink.processor.TransactionProcessor;
+import io.pixelsdb.pixels.sink.pipeline.TablePipelineManager;
+import io.pixelsdb.pixels.sink.pipeline.TransactionPipeline;
import io.pixelsdb.pixels.sink.source.SinkSource;
import java.util.Properties;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.TimeUnit;
public class SinkKafkaSource implements SinkSource
{
- private MonitorThreadManager manager;
- private volatile boolean running = true;
+ private static final long SHUTDOWN_TIMEOUT_SECONDS = 10;
+
+ private ExecutorService sourceExecutor;
+ private KafkaTransactionSource transactionSource;
+ private TopicProcessor topicProcessor;
+ private TablePipelineManager tablePipelineManager;
+ private TransactionPipeline transactionPipeline;
+ private volatile boolean running;
@Override
public void start()
@@ -45,24 +48,107 @@ public void start()
Properties transactionKafkaProperties = kafkaPropFactorySelector
.getFactory(PixelsSinkConstants.TRANSACTION_KAFKA_PROP_FACTORY)
.createKafkaProperties(pixelsSinkConfig);
- TransactionProcessor transactionProcessor = null; // TODO: new TransactionProcessor();
+ String transactionTopic = pixelsSinkConfig.getTopicPrefix() + "." +
+ pixelsSinkConfig.getTransactionTopicSuffix();
+ tablePipelineManager = new TablePipelineManager();
+ transactionPipeline = new TransactionPipeline();
+ transactionSource =
+ new KafkaTransactionSource(
+ transactionKafkaProperties, transactionTopic, transactionPipeline);
Properties topicKafkaProperties = kafkaPropFactorySelector
.getFactory(PixelsSinkConstants.ROW_RECORD_KAFKA_PROP_FACTORY)
.createKafkaProperties(pixelsSinkConfig);
- TopicProcessor topicMonitor = new TopicProcessor(pixelsSinkConfig, topicKafkaProperties);
+ topicProcessor = new TopicProcessor(
+ pixelsSinkConfig, topicKafkaProperties, tablePipelineManager);
- manager = new MonitorThreadManager();
- manager.startMonitor(transactionProcessor);
- manager.startMonitor(topicMonitor);
+ transactionPipeline.start();
+ sourceExecutor = Executors.newFixedThreadPool(2);
+ sourceExecutor.submit(transactionSource);
+ sourceExecutor.submit(topicProcessor);
+ running = true;
}
@Override
- public void stopProcessor()
+ public void close()
+ {
+ if (transactionSource != null)
+ {
+ transactionSource.requestStop();
+ }
+ if (topicProcessor != null)
+ {
+ topicProcessor.requestStop();
+ }
+
+ boolean sourcesStopped = true;
+ if (sourceExecutor != null)
+ {
+ sourceExecutor.shutdown();
+ try
+ {
+ if (!sourceExecutor.awaitTermination(
+ SHUTDOWN_TIMEOUT_SECONDS, TimeUnit.SECONDS))
+ {
+ sourceExecutor.shutdownNow();
+ sourcesStopped = false;
+ }
+ } catch (InterruptedException e)
+ {
+ sourceExecutor.shutdownNow();
+ Thread.currentThread().interrupt();
+ sourcesStopped = false;
+ }
+ }
+
+ if (transactionPipeline != null)
+ {
+ if (sourcesStopped)
+ {
+ transactionPipeline.close();
+ } else
+ {
+ transactionPipeline.abort();
+ }
+ }
+ if (tablePipelineManager != null)
+ {
+ if (sourcesStopped)
+ {
+ tablePipelineManager.close();
+ } else
+ {
+ tablePipelineManager.abort();
+ }
+ }
+ running = false;
+ }
+
+ @Override
+ public void abort()
{
- manager.shutdown();
running = false;
+ if (transactionSource != null)
+ {
+ transactionSource.requestStop();
+ }
+ if (topicProcessor != null)
+ {
+ topicProcessor.abort();
+ }
+ if (sourceExecutor != null)
+ {
+ sourceExecutor.shutdownNow();
+ }
+ if (transactionPipeline != null)
+ {
+ transactionPipeline.abort();
+ }
+ if (tablePipelineManager != null)
+ {
+ tablePipelineManager.abort();
+ }
}
@Override
diff --git a/src/main/java/io/pixelsdb/pixels/sink/source/kafka/TopicProcessor.java b/src/main/java/io/pixelsdb/pixels/sink/source/kafka/TopicProcessor.java
new file mode 100644
index 0000000..3929fee
--- /dev/null
+++ b/src/main/java/io/pixelsdb/pixels/sink/source/kafka/TopicProcessor.java
@@ -0,0 +1,207 @@
+/*
+ * Copyright 2026 PixelsDB.
+ *
+ * Licensed 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 io.pixelsdb.pixels.sink.source.kafka;
+
+import io.pixelsdb.pixels.sink.config.PixelsSinkConfig;
+import io.pixelsdb.pixels.sink.pipeline.TablePipelineManager;
+import org.apache.kafka.clients.admin.AdminClient;
+import org.apache.kafka.clients.admin.AdminClientConfig;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.time.Duration;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Map;
+import java.util.Properties;
+import java.util.Set;
+import java.util.Timer;
+import java.util.TimerTask;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.stream.Collectors;
+
+public final class TopicProcessor implements Runnable
+{
+ private static final Logger LOGGER = LoggerFactory.getLogger(TopicProcessor.class);
+
+ private final Properties kafkaProperties;
+ private final PixelsSinkConfig config;
+ private final String[] includeTables;
+ private final String bootstrapServers;
+ private final String baseTopic;
+ private final TablePipelineManager tablePipelineManager;
+ private final Set subscribedTopics = ConcurrentHashMap.newKeySet();
+ private final Map activeSources = new ConcurrentHashMap<>();
+ private final ExecutorService executor = Executors.newCachedThreadPool();
+ private final AtomicBoolean running = new AtomicBoolean(true);
+ private final Object sourceLifecycleLock = new Object();
+ private AdminClient adminClient;
+ private Timer timer;
+
+ public TopicProcessor(
+ PixelsSinkConfig config,
+ Properties kafkaProperties,
+ TablePipelineManager tablePipelineManager)
+ {
+ this.config = config;
+ this.kafkaProperties = kafkaProperties;
+ this.includeTables = config.getIncludeTables();
+ this.bootstrapServers = config.getBootstrapServers();
+ this.baseTopic = config.getTopicPrefix() + "." + config.getCaptureDatabase();
+ this.tablePipelineManager = tablePipelineManager;
+ }
+
+ @Override
+ public void run()
+ {
+ try
+ {
+ Properties adminProperties = new Properties();
+ adminProperties.put(
+ AdminClientConfig.BOOTSTRAP_SERVERS_CONFIG, bootstrapServers);
+ adminClient = AdminClient.create(adminProperties);
+ timer = new Timer("TopicProcessor-Timer", true);
+ timer.scheduleAtFixedRate(new TopicMonitorTask(), 0, 5000);
+ while (running.get())
+ {
+ try
+ {
+ TimeUnit.SECONDS.sleep(1);
+ } catch (InterruptedException e)
+ {
+ if (running.get())
+ {
+ LOGGER.warn("Kafka topic monitor interrupted", e);
+ }
+ Thread.currentThread().interrupt();
+ break;
+ }
+ }
+ } finally
+ {
+ requestStop();
+ }
+ }
+
+ void requestStop()
+ {
+ if (!running.compareAndSet(true, false))
+ {
+ return;
+ }
+ if (timer != null)
+ {
+ timer.cancel();
+ }
+ if (adminClient != null)
+ {
+ adminClient.close(Duration.ofSeconds(5));
+ }
+ synchronized (sourceLifecycleLock)
+ {
+ activeSources.values().forEach(KafkaRowSource::requestStop);
+ activeSources.clear();
+ executor.shutdown();
+ }
+ try
+ {
+ if (!executor.awaitTermination(10, TimeUnit.SECONDS))
+ {
+ executor.shutdownNow();
+ }
+ } catch (InterruptedException e)
+ {
+ executor.shutdownNow();
+ Thread.currentThread().interrupt();
+ }
+ }
+
+ void abort()
+ {
+ running.set(false);
+ if (timer != null)
+ {
+ timer.cancel();
+ }
+ if (adminClient != null)
+ {
+ adminClient.close(Duration.ofSeconds(5));
+ }
+ synchronized (sourceLifecycleLock)
+ {
+ activeSources.values().forEach(KafkaRowSource::requestStop);
+ activeSources.clear();
+ executor.shutdownNow();
+ }
+ }
+
+ private class TopicMonitorTask extends TimerTask
+ {
+ @Override
+ public void run()
+ {
+ if (!running.get())
+ {
+ cancel();
+ return;
+ }
+ try
+ {
+ Set topics = adminClient.listTopics().names().get(5, TimeUnit.SECONDS)
+ .stream()
+ .filter(topic -> topic.startsWith(baseTopic + "."))
+ .collect(Collectors.toSet());
+ topics.stream()
+ .filter(topic -> !subscribedTopics.contains(topic))
+ .filter(TopicProcessor.this::shouldProcessTable)
+ .forEach(this::startTopic);
+ } catch (Exception e)
+ {
+ if (running.get())
+ {
+ LOGGER.warn("Failed to inspect Kafka topics", e);
+ }
+ }
+ }
+
+ private void startTopic(String topic)
+ {
+ synchronized (sourceLifecycleLock)
+ {
+ if (!running.get() || executor.isShutdown())
+ {
+ return;
+ }
+ KafkaRowSource source = new KafkaRowSource(
+ kafkaProperties, topic, tablePipelineManager);
+ activeSources.put(topic, source);
+ subscribedTopics.add(topic);
+ executor.submit(source);
+ }
+ }
+ }
+
+ private boolean shouldProcessTable(String topic)
+ {
+ String tableName = topic.substring(topic.lastIndexOf('.') + 1);
+ return includeTables.length == 0 ||
+ Arrays.stream(includeTables).anyMatch(tableName::equals);
+ }
+}
diff --git a/src/main/java/io/pixelsdb/pixels/sink/source/kafka/serde/KafkaRecordConverter.java b/src/main/java/io/pixelsdb/pixels/sink/source/kafka/serde/KafkaRecordConverter.java
new file mode 100644
index 0000000..44f3d86
--- /dev/null
+++ b/src/main/java/io/pixelsdb/pixels/sink/source/kafka/serde/KafkaRecordConverter.java
@@ -0,0 +1,206 @@
+/*
+ * Copyright 2026 PixelsDB.
+ *
+ * Licensed 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 io.pixelsdb.pixels.sink.source.kafka.serde;
+
+import io.pixelsdb.pixels.sink.SinkProto;
+import io.pixelsdb.pixels.sink.config.KafkaValueFormat;
+import io.pixelsdb.pixels.sink.config.PixelsSinkConstants;
+import io.pixelsdb.pixels.sink.conversion.debezium.avro.DebeziumAvroPayloadDecoder;
+import io.pixelsdb.pixels.sink.conversion.debezium.avro.DebeziumAvroRowConverter;
+import io.pixelsdb.pixels.sink.conversion.debezium.avro.DebeziumAvroTransactionConverter;
+import io.pixelsdb.pixels.sink.conversion.debezium.dialect.DebeziumSourceAdapter;
+import io.pixelsdb.pixels.sink.conversion.debezium.dialect.DebeziumSourceAdapterRegistry;
+import io.pixelsdb.pixels.sink.conversion.debezium.json.DebeziumJsonRowConverter;
+import io.pixelsdb.pixels.sink.conversion.debezium.json.DebeziumJsonTransactionConverter;
+import io.pixelsdb.pixels.sink.event.RowChangeEvent;
+import io.pixelsdb.pixels.sink.metadata.TableMetadataRegistry;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Properties;
+
+/**
+ * Assembles Debezium row/tx converters for Kafka by {@code sink.kafka.value.format}.
+ */
+public final class KafkaRecordConverter implements AutoCloseable
+{
+ private static final Logger LOGGER = LoggerFactory.getLogger(KafkaRecordConverter.class);
+
+ private enum ErrorPolicy
+ {
+ SWALLOW,
+ PROPAGATE
+ }
+
+ @FunctionalInterface
+ private interface ConvertFn
+ {
+ T convert(String topic, byte[] data) throws Exception;
+ }
+
+ private final ConvertFn convertFn;
+ private final AutoCloseable resource;
+ private final ErrorPolicy errorPolicy;
+
+ private KafkaRecordConverter(
+ ConvertFn convertFn, AutoCloseable resource, ErrorPolicy errorPolicy)
+ {
+ this.convertFn = convertFn;
+ this.resource = resource;
+ this.errorPolicy = errorPolicy;
+ }
+
+ public static KafkaRecordConverter forRow(Properties properties)
+ {
+ String format = resolveFormat(properties);
+ DebeziumSourceAdapter adapter = resolveAdapter(properties);
+ return switch (format)
+ {
+ case KafkaValueFormat.JSON ->
+ {
+ DebeziumJsonRowConverter converter = new DebeziumJsonRowConverter(
+ TableMetadataRegistry.Instance(), adapter);
+ yield new KafkaRecordConverter<>(
+ (topic, data) -> converter.convert(data), null, ErrorPolicy.SWALLOW);
+ }
+ case KafkaValueFormat.AVRO ->
+ {
+ DebeziumAvroPayloadDecoder decoder = new DebeziumAvroPayloadDecoder();
+ decoder.configure(toConfigMap(properties), false);
+ DebeziumAvroRowConverter converter = new DebeziumAvroRowConverter(
+ TableMetadataRegistry.Instance(), adapter);
+ yield new KafkaRecordConverter<>(
+ (topic, data) -> converter.convert(decoder.decode(topic, data)),
+ decoder,
+ ErrorPolicy.SWALLOW);
+ }
+ default -> throw new IllegalArgumentException(
+ "Unsupported Kafka value format: " + format);
+ };
+ }
+
+ public static KafkaRecordConverter forTransaction(
+ Properties properties)
+ {
+ String format = resolveFormat(properties);
+ // Transaction envelopes lack source.connector; dialect must be configured up front.
+ DebeziumSourceAdapter adapter = requireAdapter(properties);
+ return switch (format)
+ {
+ case KafkaValueFormat.JSON ->
+ {
+ DebeziumJsonTransactionConverter converter =
+ new DebeziumJsonTransactionConverter(adapter);
+ yield new KafkaRecordConverter<>(
+ (topic, data) -> converter.convert(data), null, ErrorPolicy.PROPAGATE);
+ }
+ case KafkaValueFormat.AVRO ->
+ {
+ DebeziumAvroPayloadDecoder decoder = new DebeziumAvroPayloadDecoder();
+ decoder.configure(toConfigMap(properties), false);
+ DebeziumAvroTransactionConverter converter =
+ new DebeziumAvroTransactionConverter(adapter);
+ yield new KafkaRecordConverter<>(
+ (topic, data) -> converter.convert(decoder.decode(topic, data)),
+ decoder,
+ ErrorPolicy.PROPAGATE);
+ }
+ default -> throw new IllegalArgumentException(
+ "Unsupported Kafka value format: " + format);
+ };
+ }
+
+ public T convert(String topic, byte[] data)
+ {
+ if (data == null || data.length == 0)
+ {
+ return null;
+ }
+ try
+ {
+ return convertFn.convert(topic, data);
+ } catch (RuntimeException e)
+ {
+ if (errorPolicy == ErrorPolicy.SWALLOW)
+ {
+ LOGGER.warn("Failed to convert Kafka record from topic {}", topic, e);
+ return null;
+ }
+ throw e;
+ } catch (Exception e)
+ {
+ if (errorPolicy == ErrorPolicy.SWALLOW)
+ {
+ LOGGER.warn("Failed to convert Kafka record from topic {}", topic, e);
+ return null;
+ }
+ throw new RuntimeException("Failed to convert Kafka record from " + topic, e);
+ }
+ }
+
+ @Override
+ public void close()
+ {
+ if (resource != null)
+ {
+ try
+ {
+ resource.close();
+ } catch (Exception e)
+ {
+ LOGGER.warn("Failed to close Kafka record converter resource", e);
+ }
+ }
+ }
+
+ private static String resolveFormat(Properties properties)
+ {
+ Object configured = properties.get(PixelsSinkConstants.KAFKA_VALUE_FORMAT);
+ return KafkaValueFormat.resolve(configured == null ? null : configured.toString());
+ }
+
+ private static DebeziumSourceAdapter resolveAdapter(Properties properties)
+ {
+ Object dialect = properties.get(PixelsSinkConstants.SINK_DEBEZIUM_DIALECT);
+ if (dialect == null || dialect.toString().isBlank())
+ {
+ return null;
+ }
+ return DebeziumSourceAdapterRegistry.resolve(dialect.toString());
+ }
+
+ private static DebeziumSourceAdapter requireAdapter(Properties properties)
+ {
+ DebeziumSourceAdapter adapter = resolveAdapter(properties);
+ if (adapter == null)
+ {
+ throw new IllegalStateException(
+ PixelsSinkConstants.SINK_DEBEZIUM_DIALECT +
+ " is required for Kafka transaction decoding" +
+ " (mysql or postgresql)");
+ }
+ return adapter;
+ }
+
+ private static Map toConfigMap(Properties properties)
+ {
+ Map configuration = new HashMap<>();
+ properties.forEach((key, value) -> configuration.put(key.toString(), value));
+ return configuration;
+ }
+}
diff --git a/src/main/java/io/pixelsdb/pixels/sink/source/kafka/serde/RowRecordDeserializer.java b/src/main/java/io/pixelsdb/pixels/sink/source/kafka/serde/RowRecordDeserializer.java
new file mode 100644
index 0000000..28902ba
--- /dev/null
+++ b/src/main/java/io/pixelsdb/pixels/sink/source/kafka/serde/RowRecordDeserializer.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright 2026 PixelsDB.
+ *
+ * Licensed 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 io.pixelsdb.pixels.sink.source.kafka.serde;
+
+import com.google.protobuf.InvalidProtocolBufferException;
+import io.pixelsdb.pixels.sink.SinkProto;
+import io.pixelsdb.pixels.sink.conversion.sinkproto.RowRecordConverter;
+import io.pixelsdb.pixels.sink.event.RowChangeEvent;
+import io.pixelsdb.pixels.sink.exception.SinkException;
+import io.pixelsdb.pixels.sink.metadata.TableMetadataRegistry;
+import org.apache.kafka.common.errors.SerializationException;
+import org.apache.kafka.common.serialization.Deserializer;
+
+public final class RowRecordDeserializer implements Deserializer
+{
+ private final RowRecordConverter converter =
+ new RowRecordConverter(TableMetadataRegistry.Instance());
+
+ @Override
+ public RowChangeEvent deserialize(String topic, byte[] data)
+ {
+ if (data == null || data.length == 0)
+ {
+ return null;
+ }
+ try
+ {
+ return converter.convert(SinkProto.RowRecord.parseFrom(data));
+ } catch (InvalidProtocolBufferException | SinkException e)
+ {
+ throw new SerializationException(
+ "Failed to deserialize canonical row record from " + topic, e);
+ }
+ }
+}
diff --git a/src/main/java/io/pixelsdb/pixels/sink/source/kafka/serde/TransactionMetadataDeserializer.java b/src/main/java/io/pixelsdb/pixels/sink/source/kafka/serde/TransactionMetadataDeserializer.java
new file mode 100644
index 0000000..9b4ac1b
--- /dev/null
+++ b/src/main/java/io/pixelsdb/pixels/sink/source/kafka/serde/TransactionMetadataDeserializer.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright 2026 PixelsDB.
+ *
+ * Licensed 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 io.pixelsdb.pixels.sink.source.kafka.serde;
+
+import com.google.protobuf.InvalidProtocolBufferException;
+import io.pixelsdb.pixels.sink.SinkProto;
+import org.apache.kafka.common.errors.SerializationException;
+import org.apache.kafka.common.serialization.Deserializer;
+
+public final class TransactionMetadataDeserializer
+ implements Deserializer
+{
+ @Override
+ public SinkProto.TransactionMetadata deserialize(String topic, byte[] data)
+ {
+ if (data == null || data.length == 0)
+ {
+ return null;
+ }
+ try
+ {
+ return SinkProto.TransactionMetadata.parseFrom(data);
+ } catch (InvalidProtocolBufferException e)
+ {
+ throw new SerializationException(
+ "Failed to deserialize canonical transaction metadata from " + topic, e);
+ }
+ }
+}
diff --git a/src/main/java/io/pixelsdb/pixels/sink/source/storage/AbstractMemorySinkStorageSource.java b/src/main/java/io/pixelsdb/pixels/sink/source/storage/AbstractMemorySinkStorageSource.java
deleted file mode 100644
index d3e0f7c..0000000
--- a/src/main/java/io/pixelsdb/pixels/sink/source/storage/AbstractMemorySinkStorageSource.java
+++ /dev/null
@@ -1,150 +0,0 @@
-/*
- * Copyright 2025 PixelsDB.
- *
- * This file is part of Pixels.
- *
- * Pixels is free software: you can redistribute it and/or modify
- * it under the terms of the Affero GNU General Public License as
- * published by the Free Software Foundation, either version 3 of
- * the License, or (at your option) any later version.
- *
- * Pixels is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * Affero GNU General Public License for more details.
- *
- * You should have received a copy of the Affero GNU General Public
- * License along with Pixels. If not, see
- * .
- */
-
-package io.pixelsdb.pixels.sink.source.storage;
-
-import io.pixelsdb.pixels.common.physical.PhysicalReader;
-import io.pixelsdb.pixels.common.physical.PhysicalReaderUtil;
-import io.pixelsdb.pixels.common.physical.Storage;
-import io.pixelsdb.pixels.core.utils.Pair;
-import io.pixelsdb.pixels.sink.config.PixelsSinkConstants;
-import io.pixelsdb.pixels.sink.provider.ProtoType;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import java.io.IOException;
-import java.nio.ByteBuffer;
-import java.nio.ByteOrder;
-import java.util.ArrayList;
-import java.util.List;
-import java.util.concurrent.BlockingQueue;
-import java.util.concurrent.CompletableFuture;
-import java.util.concurrent.LinkedBlockingQueue;
-
-public abstract class AbstractMemorySinkStorageSource extends AbstractSinkStorageSource
-{
- private static final Logger LOGGER = LoggerFactory.getLogger(AbstractMemorySinkStorageSource.class);
-
- // All preloaded records, order preserved
- // key + value buffer
- private final List> preloadedRecords = new ArrayList<>();
-
- @Override
- public void start()
- {
- this.running.set(true);
- this.transactionProcessorThread.start();
- this.transactionProviderThread.start();
- try
- {
- /* =====================================================
- * 1. Initialization phase: preload all ByteBuffers
- * ===================================================== */
- for (String file : files)
- {
- Storage.Scheme scheme = Storage.Scheme.fromPath(file);
- LOGGER.info("Preloading file {}", file);
-
- PhysicalReader reader = PhysicalReaderUtil.newPhysicalReader(scheme, file);
- readers.add(reader);
-
- while (true)
- {
- int key;
- int valueLen;
-
- try
- {
- key = reader.readInt(ByteOrder.BIG_ENDIAN);
- valueLen = reader.readInt(ByteOrder.BIG_ENDIAN);
- } catch (IOException eof)
- {
- // Reached end of file
- break;
- }
- // Synchronous read and copy to heap buffer
- ByteBuffer valueBuffer = reader.readFully(valueLen);
- // Store into a single global array
- ByteBuffer cleanBuffer = valueBuffer.duplicate();
- cleanBuffer.rewind();
- cleanBuffer.limit(cleanBuffer.position() + valueLen);
- preloadedRecords.add(new Pair<>(key, cleanBuffer));
- }
- }
-
- LOGGER.info("Preload finished, total records = {}", preloadedRecords.size());
-
- /* =====================================================
- * Phase 2: Runtime loop
- * Queue initialization, consumer startup, and feeding
- * are done together in this phase
- * ===================================================== */
- do
- {
- for (Pair record : preloadedRecords)
- {
- int key = record.getLeft();
- ByteBuffer src = record.getRight();
- ByteBuffer copy = ByteBuffer.allocate(src.remaining());
- copy.put(src.duplicate().rewind());
- copy.flip();
- // Lazily create queue
- BlockingQueue, Integer>> queue =
- queueMap.computeIfAbsent(
- key,
- k -> new LinkedBlockingQueue<>(PixelsSinkConstants.MAX_QUEUE_SIZE)
- );
-
- // Lazily start consumer thread
- consumerThreads.computeIfAbsent(key, k ->
- {
- ProtoType protoType = getProtoType(k);
- Thread t = new Thread(() -> consumeQueue(k, queue, protoType));
- t.setName("consumer-" + k);
- t.start();
- return t;
- });
-
- ProtoType protoType = getProtoType(key);
- if (protoType == ProtoType.ROW)
- {
- sourceRateLimiter.acquire(1);
- }
-
- // Use completed future to keep consumer logic unchanged
- CompletableFuture future =
- CompletableFuture.completedFuture(copy);
-
- queue.put(new Pair<>(future, loopId));
- }
- ++loopId;
- } while (storageLoopEnabled && isRunning());
- } catch (IOException | IndexOutOfBoundsException e)
- {
- throw new RuntimeException(e);
- } catch (InterruptedException e)
- {
- Thread.currentThread().interrupt();
- } finally
- {
- clean();
- }
- }
-}
diff --git a/src/main/java/io/pixelsdb/pixels/sink/source/storage/AbstractReaderSinkStorageSource.java b/src/main/java/io/pixelsdb/pixels/sink/source/storage/AbstractReaderSinkStorageSource.java
deleted file mode 100644
index 8be58ea..0000000
--- a/src/main/java/io/pixelsdb/pixels/sink/source/storage/AbstractReaderSinkStorageSource.java
+++ /dev/null
@@ -1,124 +0,0 @@
-/*
- * Copyright 2025 PixelsDB.
- *
- * This file is part of Pixels.
- *
- * Pixels is free software: you can redistribute it and/or modify
- * it under the terms of the Affero GNU General Public License as
- * published by the Free Software Foundation, either version 3 of
- * the License, or (at your option) any later version.
- *
- * Pixels is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * Affero GNU General Public License for more details.
- *
- * You should have received a copy of the Affero GNU General Public
- * License along with Pixels. If not, see
- * .
- */
-
-package io.pixelsdb.pixels.sink.source.storage;
-
-import io.pixelsdb.pixels.common.physical.PhysicalReader;
-import io.pixelsdb.pixels.common.physical.PhysicalReaderUtil;
-import io.pixelsdb.pixels.common.physical.Storage;
-import io.pixelsdb.pixels.core.utils.Pair;
-import io.pixelsdb.pixels.sink.config.PixelsSinkConstants;
-import io.pixelsdb.pixels.sink.provider.ProtoType;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import java.io.IOException;
-import java.nio.ByteBuffer;
-import java.nio.ByteOrder;
-import java.util.concurrent.BlockingQueue;
-import java.util.concurrent.CompletableFuture;
-import java.util.concurrent.LinkedBlockingQueue;
-
-public abstract class AbstractReaderSinkStorageSource extends AbstractSinkStorageSource
-{
- private static final Logger LOGGER = LoggerFactory.getLogger(AbstractReaderSinkStorageSource.class);
-
- @Override
- public void start()
- {
- this.running.set(true);
- this.transactionProcessorThread.start();
- this.transactionProviderThread.start();
- for (String file : files)
- {
- Storage.Scheme scheme = Storage.Scheme.fromPath(file);
- LOGGER.info("Start read from file {}", file);
- PhysicalReader reader;
- try
- {
- reader = PhysicalReaderUtil.newPhysicalReader(scheme, file);
- } catch (IOException e)
- {
- throw new RuntimeException(e);
- }
- readers.add(reader);
- }
- do
- {
- for (PhysicalReader reader : readers)
- {
- LOGGER.info("Start Read {}", reader.getPath());
- long offset = 0;
- while (true)
- {
- try
- {
- int key, valueLen;
- reader.seek(offset);
- try
- {
- key = reader.readInt(ByteOrder.BIG_ENDIAN);
- valueLen = reader.readInt(ByteOrder.BIG_ENDIAN);
- } catch (IOException e)
- {
- // EOF
- break;
- }
-
- ProtoType protoType = getProtoType(key);
- offset += Integer.BYTES * 2;
- CompletableFuture valueFuture = reader.readAsync(offset, valueLen)
- .thenApply(this::copyToHeap)
- .thenApply(buf -> buf.order(ByteOrder.BIG_ENDIAN));
- // move offset for next record
- offset += valueLen;
-
-
- // Get or create queue
- BlockingQueue, Integer>> queue =
- queueMap.computeIfAbsent(key,
- k -> new LinkedBlockingQueue<>(PixelsSinkConstants.MAX_QUEUE_SIZE));
-
- // Put future in queue
- if (protoType.equals(ProtoType.ROW))
- {
- sourceRateLimiter.acquire(1);
- }
- queue.put(new Pair<>(valueFuture, loopId));
- // Start consumer thread if not exists
- consumerThreads.computeIfAbsent(key, k ->
- {
- Thread t = new Thread(() -> consumeQueue(k, queue, protoType));
- t.setName("consumer-" + key);
- t.start();
- return t;
- });
- } catch (IOException | InterruptedException e)
- {
- break;
- }
- }
- }
- ++loopId;
- } while (storageLoopEnabled && isRunning());
-
- clean();
- }
-}
diff --git a/src/main/java/io/pixelsdb/pixels/sink/source/storage/AbstractSinkStorageSource.java b/src/main/java/io/pixelsdb/pixels/sink/source/storage/AbstractSinkStorageSource.java
index f65ae60..1e39f60 100644
--- a/src/main/java/io/pixelsdb/pixels/sink/source/storage/AbstractSinkStorageSource.java
+++ b/src/main/java/io/pixelsdb/pixels/sink/source/storage/AbstractSinkStorageSource.java
@@ -1,37 +1,39 @@
/*
* Copyright 2025 PixelsDB.
*
- * This file is part of Pixels.
+ * Licensed 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
*
- * Pixels is free software: you can redistribute it and/or modify
- * it under the terms of the Affero GNU General Public License as
- * published by the Free Software Foundation, either version 3 of
- * the License, or (at your option) any later version.
+ * http://www.apache.org/licenses/LICENSE-2.0
*
- * Pixels is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * Affero GNU General Public License for more details.
- *
- * You should have received a copy of the Affero GNU General Public
- * License along with Pixels. If not, see
- * .
+ * 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 io.pixelsdb.pixels.sink.source.storage;
import io.pixelsdb.pixels.common.physical.PhysicalReader;
import io.pixelsdb.pixels.core.utils.Pair;
import io.pixelsdb.pixels.sink.config.PixelsSinkConfig;
+import io.pixelsdb.pixels.sink.config.PixelsSinkConstants;
import io.pixelsdb.pixels.sink.config.factory.PixelsSinkConfigFactory;
+import io.pixelsdb.pixels.sink.SinkProto;
+import io.pixelsdb.pixels.sink.conversion.sinkproto.RowRecordConverter;
+import io.pixelsdb.pixels.sink.conversion.sinkproto.TransactionMetadataConverter;
+import io.pixelsdb.pixels.sink.event.RowChangeEvent;
import io.pixelsdb.pixels.sink.metadata.TableMetadataRegistry;
-import io.pixelsdb.pixels.sink.processor.TransactionProcessor;
+import io.pixelsdb.pixels.sink.pipeline.TablePipelineManager;
+import io.pixelsdb.pixels.sink.pipeline.TransactionPipeline;
import io.pixelsdb.pixels.sink.provider.ProtoType;
-import io.pixelsdb.pixels.sink.provider.TableProviderAndProcessorPipelineManager;
-import io.pixelsdb.pixels.sink.provider.TransactionEventStorageLoopProvider;
import io.pixelsdb.pixels.sink.source.SinkSource;
import io.pixelsdb.pixels.sink.util.EtcdFileRegistry;
+import io.pixelsdb.pixels.sink.util.DataTransform;
import io.pixelsdb.pixels.sink.util.MetricsFacade;
+import io.pixelsdb.pixels.sink.util.concurrent.DecodeExecutors;
+import io.pixelsdb.pixels.sink.util.concurrent.OrderedBatchDecoder;
import io.pixelsdb.pixels.sink.util.rateLimiter.FlushRateLimiter;
import io.pixelsdb.pixels.sink.util.rateLimiter.FlushRateLimiterFactory;
import org.slf4j.Logger;
@@ -39,19 +41,32 @@
import java.io.IOException;
import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.LinkedBlockingQueue;
+import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
public abstract class AbstractSinkStorageSource implements SinkSource
{
private static final Logger LOGGER = LoggerFactory.getLogger(AbstractSinkStorageSource.class);
+ private static final int DECODE_BATCH_SIZE = 64;
+ private static final long DECODE_BATCH_WAIT_MILLIS = 5;
+ private static final long DECODE_SHUTDOWN_TIMEOUT_SECONDS = 30;
+ protected static final int RECORD_HEADER_SIZE = Integer.BYTES * 2;
protected final AtomicBoolean running = new AtomicBoolean(false);
+ private final AtomicBoolean started = new AtomicBoolean(false);
+ private final AtomicBoolean abortRequested = new AtomicBoolean(false);
+ private final CountDownLatch stopped = new CountDownLatch(1);
+ private volatile Thread sourceThread;
protected final String topic;
protected final String baseDir;
@@ -62,13 +77,14 @@ public abstract class AbstractSinkStorageSource implements SinkSource
protected final Map, Integer>>> queueMap = new ConcurrentHashMap<>();
protected final boolean storageLoopEnabled;
protected final FlushRateLimiter sourceRateLimiter;
- private final TableMetadataRegistry tableMetadataRegistry = TableMetadataRegistry.Instance();
+ protected final TablePipelineManager tablePipelineManager = new TablePipelineManager();
+ protected final TransactionPipeline transactionPipeline = new TransactionPipeline();
+ protected final RowRecordConverter rowRecordConverter;
+ protected final TransactionMetadataConverter transactionMetadataConverter =
+ new TransactionMetadataConverter();
+ protected final boolean freshnessTimestamp;
private final MetricsFacade metricsFacade = MetricsFacade.getInstance();
- private final TableProviderAndProcessorPipelineManager> tablePipelineManager = new TableProviderAndProcessorPipelineManager<>();
- protected TransactionEventStorageLoopProvider> transactionEventProvider;
- protected TransactionProcessor transactionProcessor;
- protected Thread transactionProviderThread;
- protected Thread transactionProcessorThread;
+ private final ExecutorService decodeExecutor;
protected int loopId = 0;
protected List readers = new ArrayList<>();
@@ -80,85 +96,304 @@ protected AbstractSinkStorageSource()
this.etcdFileRegistry = new EtcdFileRegistry(topic, baseDir);
this.files = this.etcdFileRegistry.listAllFiles();
this.storageLoopEnabled = pixelsSinkConfig.isSinkStorageLoop();
+ this.rowRecordConverter = new RowRecordConverter(TableMetadataRegistry.Instance());
+ this.freshnessTimestamp = pixelsSinkConfig.isSinkMonitorFreshnessTimestamp();
+ this.sourceRateLimiter = FlushRateLimiterFactory.getNewInstance();
+ this.decodeExecutor = DecodeExecutors.newFixedCallerRuns(
+ pixelsSinkConfig.getSourceDecodeThreads(),
+ "storage-proto-decoder");
+ }
+
+ protected void beginProcessing()
+ {
+ if (!started.compareAndSet(false, true))
+ {
+ throw new IllegalStateException("Storage source has already been started");
+ }
+ sourceThread = Thread.currentThread();
+ running.set(true);
+ transactionPipeline.start();
+ }
- this.transactionEventProvider = new TransactionEventStorageLoopProvider<>();
- this.transactionProviderThread = new Thread(transactionEventProvider);
+ ProtoType getProtoType(int key)
+ {
+ return key == -1 ? ProtoType.TRANS : ProtoType.ROW;
+ }
- this.transactionProcessor = new TransactionProcessor(transactionEventProvider);
- this.transactionProcessorThread = new Thread(transactionProcessor, "debezium-processor");
- this.sourceRateLimiter = FlushRateLimiterFactory.getNewInstance();
+ protected static Pair readRecord(
+ PhysicalReader reader, long offset, long fileLength) throws IOException
+ {
+ long remaining = fileLength - offset;
+ if (remaining < RECORD_HEADER_SIZE)
+ {
+ throw new IOException(
+ "Truncated sink proto header at offset " + offset +
+ " in " + reader.getPath());
+ }
+
+ int key = reader.readInt(ByteOrder.BIG_ENDIAN);
+ int valueLength = reader.readInt(ByteOrder.BIG_ENDIAN);
+ long availablePayload = remaining - RECORD_HEADER_SIZE;
+ if (valueLength < 0 || valueLength > availablePayload)
+ {
+ throw new IOException(
+ "Invalid sink proto payload length " + valueLength +
+ " at offset " + offset + " in " + reader.getPath());
+ }
+
+ return new Pair<>(key, reader.readFully(valueLength));
}
- abstract ProtoType getProtoType(int i);
+ protected void submitRecord(int key, ByteBuffer valueBuffer, int recordLoopId)
+ throws InterruptedException
+ {
+ BlockingQueue, Integer>> queue =
+ queueMap.computeIfAbsent(
+ key,
+ ignored -> new LinkedBlockingQueue<>(PixelsSinkConstants.MAX_QUEUE_SIZE)
+ );
+
+ consumerThreads.computeIfAbsent(key, ignored ->
+ {
+ ProtoType protoType = getProtoType(key);
+ Thread thread = new Thread(() -> consumeQueue(key, queue, protoType));
+ thread.setName("consumer-" + key);
+ thread.start();
+ return thread;
+ });
+
+ if (getProtoType(key) == ProtoType.ROW)
+ {
+ sourceRateLimiter.acquire(1);
+ }
+
+ queue.put(new Pair<>(
+ CompletableFuture.completedFuture(valueBuffer),
+ recordLoopId
+ ));
+ }
protected void clean()
{
- queueMap.values().forEach(q ->
+ boolean interrupted = false;
+ try
{
- try
+ running.set(false);
+ if (!abortRequested.get())
{
- q.put(new Pair<>(POISON_PILL, loopId));
- } catch (InterruptedException e)
+ for (BlockingQueue, Integer>> queue
+ : queueMap.values())
+ {
+ try
+ {
+ queue.put(new Pair<>(POISON_PILL, loopId));
+ } catch (InterruptedException e)
+ {
+ interrupted = true;
+ abortRequested.set(true);
+ break;
+ }
+ }
+ }
+ if (abortRequested.get())
{
- Thread.currentThread().interrupt();
+ consumerThreads.values().forEach(Thread::interrupt);
}
- });
- consumerThreads.values().forEach(t ->
- {
- try
+ for (Thread thread : consumerThreads.values())
{
- t.join();
- } catch (InterruptedException e)
+ while (thread.isAlive())
+ {
+ try
+ {
+ thread.join();
+ } catch (InterruptedException e)
+ {
+ interrupted = true;
+ abortRequested.set(true);
+ consumerThreads.values().forEach(Thread::interrupt);
+ }
+ }
+ }
+
+ if (abortRequested.get())
{
- Thread.currentThread().interrupt();
+ decodeExecutor.shutdownNow();
+ } else
+ {
+ shutdownDecodeExecutor();
}
- });
- for (PhysicalReader reader : readers)
- {
- try
+ for (PhysicalReader reader : readers)
{
- reader.close();
- } catch (IOException e)
+ try
+ {
+ reader.close();
+ } catch (IOException e)
+ {
+ LOGGER.warn("Failed to close reader", e);
+ }
+ }
+ if (abortRequested.get())
+ {
+ tablePipelineManager.abort();
+ transactionPipeline.abort();
+ } else
+ {
+ tablePipelineManager.close();
+ transactionPipeline.close();
+ }
+ } finally
+ {
+ sourceThread = null;
+ stopped.countDown();
+ if (interrupted)
{
- LOGGER.warn("Failed to close reader", e);
+ Thread.currentThread().interrupt();
}
}
}
- protected void handleTransactionSourceRecord(ByteBuffer record, Integer loopId)
+ private void shutdownDecodeExecutor()
{
- transactionEventProvider.putTransRawEvent(new Pair<>(record, loopId));
+ DecodeExecutors.shutdownGracefully(
+ decodeExecutor, DECODE_SHUTDOWN_TIMEOUT_SECONDS, LOGGER);
}
protected void consumeQueue(int key, BlockingQueue, Integer>> queue, ProtoType protoType)
{
+ boolean stopAfterBatch = false;
try
{
- while (true)
+ while (!stopAfterBatch)
{
- Pair, Integer> pair = queue.take();
- CompletableFuture value = pair.getLeft();
- int loopId = pair.getRight();
- if (value == POISON_PILL)
+ List, Integer>> batch =
+ new ArrayList<>(DECODE_BATCH_SIZE);
+ Pair, Integer> first = queue.take();
+ if (isPoisonPill(first))
{
break;
}
- ByteBuffer valueBuffer = value.get();
- metricsFacade.recordDebeziumEvent();
- switch (protoType)
+ batch.add(first);
+
+ long deadline = System.nanoTime() +
+ TimeUnit.MILLISECONDS.toNanos(DECODE_BATCH_WAIT_MILLIS);
+ while (batch.size() < DECODE_BATCH_SIZE)
{
- case ROW -> handleRowChangeSourceRecord(key, valueBuffer, loopId);
- case TRANS -> handleTransactionSourceRecord(valueBuffer, loopId);
+ long remainingNanos = deadline - System.nanoTime();
+ if (remainingNanos <= 0)
+ {
+ break;
+ }
+
+ Pair, Integer> next =
+ queue.poll(remainingNanos, TimeUnit.NANOSECONDS);
+ if (next == null)
+ {
+ break;
+ }
+ if (isPoisonPill(next))
+ {
+ stopAfterBatch = true;
+ break;
+ }
+ batch.add(next);
}
+
+ processBatch(key, batch, protoType);
}
} catch (InterruptedException e)
{
Thread.currentThread().interrupt();
+ }
+ }
+
+ private boolean isPoisonPill(
+ Pair, Integer> record)
+ {
+ return record.getLeft() == POISON_PILL;
+ }
+
+ private void processBatch(
+ int key,
+ List, Integer>> batch,
+ ProtoType protoType) throws InterruptedException
+ {
+ for (int i = 0; i < batch.size(); ++i)
+ {
+ metricsFacade.recordDebeziumEvent();
+ }
+
+ switch (protoType)
+ {
+ case ROW ->
+ {
+ List events = OrderedBatchDecoder.decodeInOrder(
+ decodeExecutor,
+ batch,
+ record -> decodeRowChangeSourceRecord(key, record));
+ for (RowChangeEvent event : events)
+ {
+ if (event != null)
+ {
+ metricsFacade.recordSerdRowChange();
+ tablePipelineManager.route(event);
+ }
+ }
+ }
+ case TRANS ->
+ {
+ List transactions =
+ OrderedBatchDecoder.decodeInOrder(
+ decodeExecutor,
+ batch,
+ this::decodeTransactionSourceRecord);
+ for (SinkProto.TransactionMetadata transaction : transactions)
+ {
+ if (transaction != null)
+ {
+ metricsFacade.recordSerdTxChange();
+ transactionPipeline.publish(transaction);
+ }
+ }
+ }
+ }
+ }
+
+ private RowChangeEvent decodeRowChangeSourceRecord(
+ int key,
+ Pair, Integer> record)
+ {
+ try
+ {
+ return convertRowChangeSourceRecord(
+ key, record.getLeft().get(), record.getRight());
+ } catch (InterruptedException e)
+ {
+ Thread.currentThread().interrupt();
+ return null;
} catch (ExecutionException e)
{
- LOGGER.error("Error in async processing", e);
+ LOGGER.warn("Failed to read storage row record", e.getCause());
+ return null;
+ }
+ }
+
+ private SinkProto.TransactionMetadata decodeTransactionSourceRecord(
+ Pair, Integer> record)
+ {
+ try
+ {
+ return transactionMetadataConverter.convert(
+ record.getLeft().get(), record.getRight());
+ } catch (InterruptedException e)
+ {
+ Thread.currentThread().interrupt();
+ return null;
+ } catch (Exception e)
+ {
+ LOGGER.warn("Failed to convert storage transaction metadata", e);
+ return null;
}
}
@@ -171,9 +406,30 @@ protected ByteBuffer copyToHeap(ByteBuffer directBuffer)
return heapBuffer;
}
- protected void handleRowChangeSourceRecord(int key, ByteBuffer dataBuffer, int loopId)
+ protected RowChangeEvent convertRowChangeSourceRecord(
+ int key, ByteBuffer dataBuffer, int loopId)
{
- tablePipelineManager.routeRecord(key, new Pair<>(dataBuffer, loopId));
+ try
+ {
+ SinkProto.RowRecord.Builder builder =
+ rowRecordConverter.parse(dataBuffer).toBuilder();
+ if (freshnessTimestamp)
+ {
+ DataTransform.updateRecordTimestamp(builder, System.currentTimeMillis() * 1000);
+ }
+ if (builder.hasTransaction())
+ {
+ SinkProto.TransactionInfo transaction = builder.getTransaction();
+ builder.setTransaction(transaction.toBuilder()
+ .setId(transaction.getId() + "_" + loopId)
+ .build());
+ }
+ return rowRecordConverter.convert(builder.build());
+ } catch (Exception e)
+ {
+ LOGGER.warn("Failed to convert storage row record", e);
+ return null;
+ }
}
@Override
@@ -183,11 +439,46 @@ public boolean isRunning()
}
@Override
- public void stopProcessor()
+ public void close()
{
running.set(false);
- transactionProviderThread.interrupt();
- transactionProcessorThread.interrupt();
- transactionProcessor.stopProcessor();
+ if (!started.get() || Thread.currentThread() == sourceThread)
+ {
+ return;
+ }
+
+ boolean interrupted = false;
+ while (true)
+ {
+ try
+ {
+ stopped.await();
+ break;
+ } catch (InterruptedException e)
+ {
+ interrupted = true;
+ }
+ }
+ if (interrupted)
+ {
+ Thread.currentThread().interrupt();
+ }
+ }
+
+ @Override
+ public void abort()
+ {
+ abortRequested.set(true);
+ running.set(false);
+ consumerThreads.values().forEach(Thread::interrupt);
+ decodeExecutor.shutdownNow();
+ tablePipelineManager.abort();
+ transactionPipeline.abort();
+
+ Thread thread = sourceThread;
+ if (thread != null && thread != Thread.currentThread())
+ {
+ thread.interrupt();
+ }
}
}
diff --git a/src/main/java/io/pixelsdb/pixels/sink/source/storage/FasterSinkStorageSource.java b/src/main/java/io/pixelsdb/pixels/sink/source/storage/FasterSinkStorageSource.java
deleted file mode 100644
index fc49922..0000000
--- a/src/main/java/io/pixelsdb/pixels/sink/source/storage/FasterSinkStorageSource.java
+++ /dev/null
@@ -1,65 +0,0 @@
-/*
- * Copyright 2025 PixelsDB.
- *
- * This file is part of Pixels.
- *
- * Pixels is free software: you can redistribute it and/or modify
- * it under the terms of the Affero GNU General Public License as
- * published by the Free Software Foundation, either version 3 of
- * the License, or (at your option) any later version.
- *
- * Pixels is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * Affero GNU General Public License for more details.
- *
- * You should have received a copy of the Affero GNU General Public
- * License along with Pixels. If not, see
- * .
- */
-
-package io.pixelsdb.pixels.sink.source.storage;
-
-
-import io.pixelsdb.pixels.common.metadata.SchemaTableName;
-import io.pixelsdb.pixels.sink.provider.ProtoType;
-import io.pixelsdb.pixels.sink.source.SinkSource;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import java.nio.ByteBuffer;
-
-/**
- * @package: io.pixelsdb.pixels.sink.source
- * @className: LegacySinkStorageSource
- * @author: AntiO2
- * @date: 2025/10/5 11:43
- */
-public class FasterSinkStorageSource extends AbstractMemorySinkStorageSource implements SinkSource
-{
- private static final Logger LOGGER = LoggerFactory.getLogger(FasterSinkStorageSource.class);
- static SchemaTableName transactionSchemaTableName = new SchemaTableName("freak", "transaction");
-
- public FasterSinkStorageSource()
- {
- super();
- }
-
- private static String readString(ByteBuffer buffer, int len)
- {
- byte[] bytes = new byte[len];
- buffer.get(bytes);
- return new String(bytes);
- }
-
- @Override
- ProtoType getProtoType(int i)
- {
- if (i == -1)
- {
- return ProtoType.TRANS;
- }
- return ProtoType.ROW;
- }
-
-}
diff --git a/src/main/java/io/pixelsdb/pixels/sink/source/storage/LegacySinkStorageSource.java b/src/main/java/io/pixelsdb/pixels/sink/source/storage/LegacySinkStorageSource.java
deleted file mode 100644
index a8d636b..0000000
--- a/src/main/java/io/pixelsdb/pixels/sink/source/storage/LegacySinkStorageSource.java
+++ /dev/null
@@ -1,247 +0,0 @@
-/*
- * Copyright 2025 PixelsDB.
- *
- * This file is part of Pixels.
- *
- * Pixels is free software: you can redistribute it and/or modify
- * it under the terms of the Affero GNU General Public License as
- * published by the Free Software Foundation, either version 3 of
- * the License, or (at your option) any later version.
- *
- * Pixels is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * Affero GNU General Public License for more details.
- *
- * You should have received a copy of the Affero GNU General Public
- * License along with Pixels. If not, see
- * .
- */
-
-package io.pixelsdb.pixels.sink.source.storage;
-
-
-import io.pixelsdb.pixels.common.metadata.SchemaTableName;
-import io.pixelsdb.pixels.common.physical.PhysicalReader;
-import io.pixelsdb.pixels.common.physical.PhysicalReaderUtil;
-import io.pixelsdb.pixels.common.physical.Storage;
-import io.pixelsdb.pixels.core.utils.Pair;
-import io.pixelsdb.pixels.sink.metadata.TableMetadataRegistry;
-import io.pixelsdb.pixels.sink.processor.TransactionProcessor;
-import io.pixelsdb.pixels.sink.provider.ProtoType;
-import io.pixelsdb.pixels.sink.provider.TableProviderAndProcessorPipelineManager;
-import io.pixelsdb.pixels.sink.provider.TransactionEventEngineProvider;
-import io.pixelsdb.pixels.sink.source.SinkSource;
-import io.pixelsdb.pixels.sink.util.MetricsFacade;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import java.io.IOException;
-import java.nio.ByteBuffer;
-import java.nio.ByteOrder;
-import java.util.Map;
-import java.util.concurrent.*;
-
-/**
- * @package: io.pixelsdb.pixels.sink.source
- * @className: LegacySinkStorageSource
- * @author: AntiO2
- * @date: 2025/10/5 11:43
- */
-@Deprecated
-public class LegacySinkStorageSource extends AbstractReaderSinkStorageSource implements SinkSource
-{
- private static final Logger LOGGER = LoggerFactory.getLogger(LegacySinkStorageSource.class);
- static SchemaTableName transactionSchemaTableName = new SchemaTableName("freak", "transaction");
- private final TransactionEventEngineProvider transactionEventProvider = TransactionEventEngineProvider.INSTANCE;
-
- private final TableProviderAndProcessorPipelineManager tableProvidersManagerImpl = new TableProviderAndProcessorPipelineManager<>();
- private final TransactionProcessor transactionProcessor = new TransactionProcessor(transactionEventProvider);
- private final MetricsFacade metricsFacade = MetricsFacade.getInstance();
- private final Map>> queueMap = new ConcurrentHashMap<>();
- private final Map consumerThreads = new ConcurrentHashMap<>();
- private final int maxQueueCapacity = 10000;
- private final TableMetadataRegistry tableMetadataRegistry = TableMetadataRegistry.Instance();
- private final CompletableFuture POISON_PILL = new CompletableFuture<>();
-
-
- private static String readString(ByteBuffer buffer, int len)
- {
- byte[] bytes = new byte[len];
- buffer.get(bytes);
- return new String(bytes);
- }
-
- @Override
- ProtoType getProtoType(int i)
- {
- return ProtoType.fromInt(i);
- }
-
- @Override
- public void start()
- {
-
- for (String file : files)
- {
- Storage.Scheme scheme = Storage.Scheme.fromPath(file);
- LOGGER.info("Start read from file {}", file);
- try (PhysicalReader reader = PhysicalReaderUtil.newPhysicalReader(scheme, file))
- {
- long offset = 0;
- BlockingQueue>> rowQueue = new LinkedBlockingQueue<>();
- BlockingQueue> transQueue = new LinkedBlockingQueue<>();
- while (true)
- {
- try
- {
- int keyLen, valueLen;
- reader.seek(offset);
- try
- {
- keyLen = reader.readInt(ByteOrder.BIG_ENDIAN);
- valueLen = reader.readInt(ByteOrder.BIG_ENDIAN);
- } catch (IOException e)
- {
- // EOF
- break;
- }
-
- ByteBuffer keyBuffer = copyToHeap(reader.readFully(keyLen)).order(ByteOrder.BIG_ENDIAN);
- ProtoType protoType = getProtoType(keyBuffer.getInt());
- offset += Integer.BYTES * 2 + keyLen;
- CompletableFuture valueFuture = reader.readAsync(offset, valueLen)
- .thenApply(this::copyToHeap)
- .thenApply(buf -> buf.order(ByteOrder.BIG_ENDIAN));
- // move offset for next record
- offset += valueLen;
-
- // Compute queue key (for example: schemaName + tableName or protoType)
- SchemaTableName queueKey = computeQueueKey(keyBuffer, protoType);
-
- // Get or create queue
- BlockingQueue> queue =
- queueMap.computeIfAbsent(queueKey,
- k -> new LinkedBlockingQueue<>(maxQueueCapacity));
-
- // Put future in queue
- queue.put(valueFuture);
-
- // Start consumer thread if not exists
- consumerThreads.computeIfAbsent(queueKey, k ->
- {
- Thread t = new Thread(() -> consumeQueue(k, queue, protoType));
- t.setName("consumer-" + queueKey);
- t.start();
- return t;
- });
- } catch (IOException | InterruptedException e)
- {
- break;
- }
- }
- } catch (IOException e)
- {
- throw new RuntimeException(e);
- }
-
- }
-
- // signal all queues to stop
- queueMap.values().forEach(q ->
- {
- try
- {
- q.put(POISON_PILL);
- } catch (InterruptedException e)
- {
- Thread.currentThread().interrupt();
- }
- });
-
- // wait all consumers to finish
- consumerThreads.values().forEach(t ->
- {
- try
- {
- t.join();
- } catch (InterruptedException e)
- {
- Thread.currentThread().interrupt();
- }
- });
- }
-
- private void consumeQueue(SchemaTableName key, BlockingQueue> queue, ProtoType protoType)
- {
- try
- {
- while (true)
- {
- CompletableFuture value = queue.take();
- if (value == POISON_PILL)
- {
- break;
- }
- ByteBuffer valueBuffer = value.get();
- metricsFacade.recordDebeziumEvent();
- switch (protoType)
- {
- case ROW -> handleRowChangeSourceRecord(0, valueBuffer, 0);
- case TRANS -> handleTransactionSourceRecord(valueBuffer, 0);
- }
- }
- } catch (InterruptedException e)
- {
- Thread.currentThread().interrupt();
- } catch (ExecutionException e)
- {
- LOGGER.error("Error in async processing", e);
- }
- }
-
- private SchemaTableName computeQueueKey(ByteBuffer keyBuffer, ProtoType protoType)
- {
- switch (protoType)
- {
- case ROW ->
- {
- int schemaLen = keyBuffer.getInt();
- int tableLen = keyBuffer.getInt();
- String schemaName = readString(keyBuffer, schemaLen);
- String tableName = readString(keyBuffer, tableLen);
- return new SchemaTableName(schemaName, tableName);
- }
- case TRANS ->
- {
- return transactionSchemaTableName;
- }
- default ->
- {
- throw new IllegalArgumentException("Proto type " + protoType.toString());
- }
- }
- }
-
- private void handleRowChangeSourceRecord(SchemaTableName schemaTableName, ByteBuffer dataBuffer)
- {
- tableProvidersManagerImpl.routeRecord(schemaTableName, dataBuffer);
- }
-
- private void handleRowChangeSourceRecord(ByteBuffer keyBuffer, ByteBuffer dataBuffer)
- {
- {
- // CODE BLOCK VERSION 2
-// long tableId = keyBuffer.getLong();
-// try
-// {
-// schemaTableName = tableMetadataRegistry.getSchemaTableName(tableId);
-// } catch (SinkException e)
-// {
-// throw new RuntimeException(e);
-// }
- }
-
-// tableProvidersManagerImpl.routeRecord(schemaTableName, dataBuffer);
- }
-}
diff --git a/src/main/java/io/pixelsdb/pixels/sink/source/storage/MemorySinkStorageSource.java b/src/main/java/io/pixelsdb/pixels/sink/source/storage/MemorySinkStorageSource.java
new file mode 100644
index 0000000..b8e1b79
--- /dev/null
+++ b/src/main/java/io/pixelsdb/pixels/sink/source/storage/MemorySinkStorageSource.java
@@ -0,0 +1,110 @@
+/*
+ * Copyright 2025 PixelsDB.
+ *
+ * Licensed 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 io.pixelsdb.pixels.sink.source.storage;
+
+import io.pixelsdb.pixels.common.physical.PhysicalReader;
+import io.pixelsdb.pixels.common.physical.PhysicalReaderUtil;
+import io.pixelsdb.pixels.common.physical.Storage;
+import io.pixelsdb.pixels.core.utils.Pair;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.util.ArrayList;
+import java.util.List;
+
+public class MemorySinkStorageSource extends AbstractSinkStorageSource
+{
+ private static final Logger LOGGER = LoggerFactory.getLogger(MemorySinkStorageSource.class);
+
+ // All preloaded records, order preserved
+ // key + value buffer
+ private final List> preloadedRecords = new ArrayList<>();
+
+ @Override
+ public void start()
+ {
+ beginProcessing();
+ try
+ {
+ /* =====================================================
+ * 1. Initialization phase: preload all ByteBuffers
+ * ===================================================== */
+ for (String file : files)
+ {
+ if (!isRunning())
+ {
+ break;
+ }
+ Storage.Scheme scheme = Storage.Scheme.fromPath(file);
+ LOGGER.info("Preloading file {}", file);
+
+ PhysicalReader reader = PhysicalReaderUtil.newPhysicalReader(scheme, file);
+ readers.add(reader);
+
+ reader.seek(0);
+ long offset = 0;
+ long fileLength = reader.getFileLength();
+ while (isRunning() && offset < fileLength)
+ {
+ Pair record = readRecord(reader, offset, fileLength);
+ int valueLength = record.getRight().remaining();
+ // Store into a single global array
+ ByteBuffer cleanBuffer = record.getRight().duplicate();
+ cleanBuffer.rewind();
+ cleanBuffer.limit(cleanBuffer.position() + valueLength);
+ preloadedRecords.add(new Pair<>(record.getLeft(), cleanBuffer));
+ offset += RECORD_HEADER_SIZE + (long) valueLength;
+ }
+ }
+
+ LOGGER.info("Preload finished, total records = {}", preloadedRecords.size());
+
+ /* =====================================================
+ * Phase 2: Runtime loop
+ * Queue initialization, consumer startup, and feeding
+ * are done together in this phase
+ * ===================================================== */
+ do
+ {
+ for (Pair record : preloadedRecords)
+ {
+ if (!isRunning())
+ {
+ break;
+ }
+ int key = record.getLeft();
+ ByteBuffer src = record.getRight();
+ ByteBuffer copy = ByteBuffer.allocate(src.remaining());
+ copy.put(src.duplicate().rewind());
+ copy.flip();
+ submitRecord(key, copy, loopId);
+ }
+ ++loopId;
+ } while (storageLoopEnabled && isRunning());
+ } catch (IOException | IndexOutOfBoundsException e)
+ {
+ throw new RuntimeException(e);
+ } catch (InterruptedException e)
+ {
+ Thread.currentThread().interrupt();
+ } finally
+ {
+ clean();
+ }
+ }
+}
diff --git a/src/main/java/io/pixelsdb/pixels/sink/source/storage/StreamingSinkStorageSource.java b/src/main/java/io/pixelsdb/pixels/sink/source/storage/StreamingSinkStorageSource.java
new file mode 100644
index 0000000..a9100c6
--- /dev/null
+++ b/src/main/java/io/pixelsdb/pixels/sink/source/storage/StreamingSinkStorageSource.java
@@ -0,0 +1,82 @@
+/*
+ * Copyright 2026 PixelsDB.
+ *
+ * Licensed 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 io.pixelsdb.pixels.sink.source.storage;
+
+import io.pixelsdb.pixels.common.physical.PhysicalReader;
+import io.pixelsdb.pixels.common.physical.PhysicalReaderUtil;
+import io.pixelsdb.pixels.common.physical.Storage;
+import io.pixelsdb.pixels.core.utils.Pair;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.nio.ByteBuffer;
+
+public class StreamingSinkStorageSource extends AbstractSinkStorageSource
+{
+ private static final Logger LOGGER = LoggerFactory.getLogger(StreamingSinkStorageSource.class);
+
+ @Override
+ public void start()
+ {
+ beginProcessing();
+ try
+ {
+ for (String file : files)
+ {
+ if (!isRunning())
+ {
+ break;
+ }
+ Storage.Scheme scheme = Storage.Scheme.fromPath(file);
+ readers.add(PhysicalReaderUtil.newPhysicalReader(scheme, file));
+ }
+
+ do
+ {
+ for (PhysicalReader reader : readers)
+ {
+ if (!isRunning())
+ {
+ break;
+ }
+
+ LOGGER.info("Start reading {}", reader.getPath());
+ reader.seek(0);
+ long offset = 0;
+ long fileLength = reader.getFileLength();
+ while (isRunning() && offset < fileLength)
+ {
+ Pair record = readRecord(reader, offset, fileLength);
+ int valueLength = record.getRight().remaining();
+ submitRecord(record.getLeft(), record.getRight(), loopId);
+ offset += RECORD_HEADER_SIZE + (long) valueLength;
+ }
+ }
+ ++loopId;
+ } while (storageLoopEnabled && isRunning());
+ } catch (IOException e)
+ {
+ throw new RuntimeException("Failed to read sink proto storage", e);
+ } catch (InterruptedException e)
+ {
+ Thread.currentThread().interrupt();
+ } finally
+ {
+ clean();
+ }
+ }
+}
diff --git a/src/main/java/io/pixelsdb/pixels/sink/util/BlockingBoundedMap.java b/src/main/java/io/pixelsdb/pixels/sink/util/BlockingBoundedMap.java
index 4f649aa..7c3825c 100644
--- a/src/main/java/io/pixelsdb/pixels/sink/util/BlockingBoundedMap.java
+++ b/src/main/java/io/pixelsdb/pixels/sink/util/BlockingBoundedMap.java
@@ -1,23 +1,18 @@
/*
* Copyright 2025 PixelsDB.
*
- * This file is part of Pixels.
+ * Licensed 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
*
- * Pixels is free software: you can redistribute it and/or modify
- * it under the terms of the Affero GNU General Public License as
- * published by the Free Software Foundation, either version 3 of
- * the License, or (at your option) any later version.
+ * http://www.apache.org/licenses/LICENSE-2.0
*
- * Pixels is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * Affero GNU General Public License for more details.
- *
- * You should have received a copy of the Affero GNU General Public
- * License along with Pixels. If not, see
- * .
+ * 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 io.pixelsdb.pixels.sink.util;
import java.util.Set;
diff --git a/src/main/java/io/pixelsdb/pixels/sink/util/BlockingBoundedQueue.java b/src/main/java/io/pixelsdb/pixels/sink/util/BlockingBoundedQueue.java
new file mode 100644
index 0000000..30de2cb
--- /dev/null
+++ b/src/main/java/io/pixelsdb/pixels/sink/util/BlockingBoundedQueue.java
@@ -0,0 +1,119 @@
+/*
+ * Copyright 2026 PixelsDB.
+ *
+ * Licensed 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 io.pixelsdb.pixels.sink.util;
+
+import java.io.Closeable;
+import java.util.concurrent.BlockingQueue;
+import java.util.concurrent.LinkedBlockingQueue;
+
+/**
+ * A bounded blocking queue with an explicit shutdown signal.
+ *
+ *
Closing the queue finishes it gracefully. Use {@link #abort()} to
+ * discard pending values and wake a blocked consumer immediately.
+ */
+public final class BlockingBoundedQueue implements Closeable
+{
+ private static final Object POISON_PILL = new Object();
+
+ private final BlockingQueue