diff --git a/.gitignore b/.gitignore index 0f4acf7..5b77bdc 100644 --- a/.gitignore +++ b/.gitignore @@ -22,7 +22,6 @@ # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml hs_err_pid* -.idea build target .classpath* @@ -48,3 +47,11 @@ resources/*.xml *.so *.o .vscode + +data/ +tmp/ + +!requirements.txt +freshness*.png +rate*.png +resulti7i/ diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..7d05e99 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,10 @@ +# 默认忽略的文件 +/shelf/ +/workspace.xml +# 基于编辑器的 HTTP 客户端请求 +/httpRequests/ +# 依赖于环境的 Maven 主目录路径 +/mavenHomeManager.xml +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml diff --git a/.idea/codeStyles/codeStyleConfig.xml b/.idea/codeStyles/codeStyleConfig.xml new file mode 100644 index 0000000..79ee123 --- /dev/null +++ b/.idea/codeStyles/codeStyleConfig.xml @@ -0,0 +1,5 @@ + + + + \ No newline at end of file diff --git a/.idea/compiler.xml b/.idea/compiler.xml new file mode 100644 index 0000000..32f4678 --- /dev/null +++ b/.idea/compiler.xml @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/encodings.xml b/.idea/encodings.xml new file mode 100644 index 0000000..aa00ffa --- /dev/null +++ b/.idea/encodings.xml @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/.idea/jarRepositories.xml b/.idea/jarRepositories.xml new file mode 100644 index 0000000..c8eb127 --- /dev/null +++ b/.idea/jarRepositories.xml @@ -0,0 +1,20 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..6732724 --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,12 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..35eb1dd --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/conf/jvm.conf b/conf/jvm.conf new file mode 100644 index 0000000..28f01e7 --- /dev/null +++ b/conf/jvm.conf @@ -0,0 +1,26 @@ +-server +-XX:+AlwaysPreTouch +-Dfile.encoding=UTF-8 +-Duser.timezone=UTC + +-Xms8g +-Xmx90g + +-XX:+UseG1GC +-XX:MaxGCPauseMillis=200 +-XX:InitiatingHeapOccupancyPercent=35 +-XX:+ParallelRefProcEnabled +-XX:+UnlockExperimentalVMOptions +-XX:+TrustFinalNonStaticFields +-XX:+DisableExplicitGC + +-Xss512k + + +-XX:+HeapDumpOnOutOfMemoryError +-XX:HeapDumpPath=/var/log/java/java_heapdump.hprof +-XX:+ExitOnOutOfMemoryError + +-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:10086 +--add-opens=java.base/sun.nio.ch=ALL-UNNAMED +--add-opens=java.base/java.nio=ALL-UNNAMED diff --git a/conf/pixels-sink.aws.properties b/conf/pixels-sink.aws.properties new file mode 100644 index 0000000..bf29d0a --- /dev/null +++ b/conf/pixels-sink.aws.properties @@ -0,0 +1,108 @@ +# engine | kafka | storage +sink.datasource=engine +# -1 means no limit, Only implement in retina sink mode yet +sink.datasource.rate.limit=1000000 +# Sink Config: retina | csv | proto | flink | none +sink.mode=proto +sink.retina.client=1 +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_100/100k_rate.csv +sink.monitor.freshness.file=/home/ubuntu/pixels-sink/resulti7i_100/100k_fresh.csv +# trino for freshness query +trino.url=jdbc:trino://realtime-kafka-2:8080/pixels/pixels_bench_sf100x +# trino.url=jdbc:trino://realtime-pixels-coordinator:8080/pixels/pixels_bench_sf10x +trino.user=pixels +trino.password=password +trino.parallel=4 +# row or txn or embed +sink.monitor.freshness.level=row +sink.monitor.freshness.embed.warmup=10 +sink.monitor.freshness.embed.static=false +sink.monitor.freshness.embed.snapshot=true +sink.monitor.freshness.embed.tablelist=loanapps,loantrans +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 +#value.deserializer=io.pixelsdb.pixels.writer.deserializer.RowChangeEventAvroDeserializer +value.deserializer=io.pixelsdb.pixels.sink.event.deserializer.RowChangeEventJsonDeserializer +# Topic & Database Config +topic.prefix=postgresql.oltp_server +consumer.capture_database=pixels_bench_sf1x +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 +#writer.retina.mode=stub +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/disk2/hybench/ +sink.proto.data=hybench1000_1 +# sink.proto.data=hybench100_3 +# 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 +#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 +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 +sink.rpc.mock.delay=20 +# debezium engine config +debezium.name=testEngine +debezium.connector.class=io.debezium.connector.postgresql.PostgresConnector +debezium.provide.transaction.metadata=true +debezium.offset.storage=org.apache.kafka.connect.storage.FileOffsetBackingStore +debezium.offset.storage.file.filename=/tmp/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/schemahistory.dat +debezium.database.hostname=realtime-pg-2 +debezium.database.port=5432 +debezium.database.user=pixels +debezium.database.password=pixels_realtime_crud +debezium.database.dbname=pixels_bench +debezium.plugin.name=pgoutput +debezium.database.server.id=1 +debezium.schema.include.list=public +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=postgresql.oltp_server +debezium.transforms=topicRouting +debezium.transforms.topicRouting.type=org.apache.kafka.connect.transforms.RegexRouter +debezium.transforms.topicRouting.regex=postgresql\\.oltp_server\\.public\\.(.*) +debezium.transforms.topicRouting.replacement=postgresql.oltp_server.pixels_bench_sf10x.$1 diff --git a/conf/pixels-sink.flink.properties b/conf/pixels-sink.flink.properties new file mode 100644 index 0000000..f0fb372 --- /dev/null +++ b/conf/pixels-sink.flink.properties @@ -0,0 +1,46 @@ +# engine | kafka | storage +sink.datasource=storage +# -1 means no limit, Only implement in retina sink mode yet +sink.datasource.rate.limit=50000 +# Sink Config: retina | csv | proto | flink | none +sink.mode=flink +## 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/tmp/test.csv +sink.monitor.freshness.file=/home/ubuntu/pixels-sink/tmp/test_freshness.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=1 +# row or txn or embed +sink.monitor.freshness.level=row +sink.monitor.freshness.embed.warmup=10 +sink.monitor.freshness.embed.static=false +sink.monitor.freshness.embed.snapshot=true +sink.monitor.freshness.embed.tablelist=loanapps,loantrans +sink.monitor.freshness.verbose=false +sink.monitor.freshness.timestamp=true +sink.storage.loop=true + +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 + +## 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 + +# Sink Metrics +sink.monitor.enable=true +sink.monitor.port=9465 +sink.monitor.report.interval=10000 +sink.monitor.freshness.interval=1000 diff --git a/conf/pixels-sink.pg.properties b/conf/pixels-sink.pg.properties new file mode 100644 index 0000000..effc171 --- /dev/null +++ b/conf/pixels-sink.pg.properties @@ -0,0 +1,106 @@ +# engine | kafka | storage +sink.datasource=engine +# -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_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 +# 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=loanapps,loantrans +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 +#value.deserializer=io.pixelsdb.pixels.writer.deserializer.RowChangeEventAvroDeserializer +value.deserializer=io.pixelsdb.pixels.sink.event.deserializer.RowChangeEventJsonDeserializer +# Topic & Database Config +topic.prefix=postgresql.oltp_server +consumer.capture_database=pixels_bench_sf1x +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 +#writer.retina.mode=stub +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 +#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 +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 +sink.rpc.mock.delay=20 +# debezium engine config +debezium.name=testEngine +debezium.connector.class=io.debezium.connector.postgresql.PostgresConnector +debezium.provide.transaction.metadata=true +debezium.offset.storage=org.apache.kafka.connect.storage.FileOffsetBackingStore +debezium.offset.storage.file.filename=/tmp/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/schemahistory.dat +debezium.database.hostname=realtime-pg-2 +debezium.database.port=5432 +debezium.database.user=pixels +debezium.database.password=pixels_realtime_crud +debezium.database.dbname=pixels_bench_sf10x +debezium.plugin.name=pgoutput +debezium.database.server.id=1 +debezium.schema.include.list=public +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=postgresql.oltp_server +debezium.transforms=topicRouting +debezium.transforms.topicRouting.type=org.apache.kafka.connect.transforms.RegexRouter +debezium.transforms.topicRouting.regex=postgresql\\.oltp_server\\.public\\.(.*) +debezium.transforms.topicRouting.replacement=postgresql.oltp_server.pixels_bench_sf10x.$1 diff --git a/develop/config/register-postgres.json.template b/develop/config/register-postgres.json.template index 283ed65..0b3884b 100644 --- a/develop/config/register-postgres.json.template +++ b/develop/config/register-postgres.json.template @@ -11,21 +11,15 @@ "database.dbname" : "pixels_realtime_crud", "schema.include.list": "public", "database.server.id": "1", - "topic.prefix": "oltp_server", + "topic.prefix": "postgresql.oltp_server", "transforms": "topicRouting", "transforms.topicRouting.type": "org.apache.kafka.connect.transforms.RegexRouter", - "transforms.topicRouting.regex": "oltp_server\\.public\\.(.*)", - "transforms.topicRouting.replacement": "oltp_server.pixels_realtime_crud.$1", + "transforms.topicRouting.regex": "postgresql.oltp_server\\.public\\.(.*)", + "transforms.topicRouting.replacement": "postgresql.oltp_server.pixels_realtime_crud.$1", - "key.converter": "io.apicurio.registry.utils.converter.AvroConverter", - "value.converter": "io.apicurio.registry.utils.converter.AvroConverter", - "key.converter.apicurio.registry.url": "http://apicurio:8080/apis/registry/v2", - "key.converter.apicurio.registry.auto-register": "true", - "key.converter.apicurio.registry.find-latest": "true", - "value.converter.apicurio.registry.url": "http://apicurio:8080/apis/registry/v2", - "value.converter.apicurio.registry.auto-register": "true", - "value.converter.apicurio.registry.find-latest": "true", - "schema.name.adjustment.mode": "avro" + "key.converter": "org.apache.kafka.connect.json.JsonConverter", + "value.converter": "org.apache.kafka.connect.json.JsonConverter", + "plugin.name": "pgoutput" } } diff --git a/develop/docker-compose.yml b/develop/docker-compose.yml index 08621ff..d919b48 100644 --- a/develop/docker-compose.yml +++ b/develop/docker-compose.yml @@ -31,7 +31,7 @@ services: - pixels_network postgres: - image: quay.io/debezium/postgres:17 # This image install plugin: postgres-decoderbufs and configure wal_level = logical + image: quay.io/debezium/postgres:16 # This image install plugin: postgres-decoderbufs and configure wal_level = logical container_name: pixels_postgres_source_db environment: POSTGRES_DB: pixels_realtime_crud @@ -75,7 +75,8 @@ services: - pixels_network pixels-sink: - image: pixels-sink:0.2.0-SNAPSHOT + image: hello-world:latest + # image: pixels-sink:0.2.0-SNAPSHOT container_name: pixels-sink volumes: - ./data:/app/data @@ -101,7 +102,7 @@ services: - pixels_network pg_debezium: - image: debezium/connect:2.7.3.Final + image: debezium/connect:3.0.0.Final ports: - "8084:8083" depends_on: diff --git a/develop/docker-monitor-compose.yml b/develop/docker-monitor-compose.yml new file mode 100644 index 0000000..180be1b --- /dev/null +++ b/develop/docker-monitor-compose.yml @@ -0,0 +1,40 @@ +services: + # monitor + prometheus: + image: prom/prometheus:v3.2.1 + container_name: pixels-prometheus + ports: + - "9090:9090" + volumes: + - ./images/prometheus/prometheus_local.yml:/etc/prometheus/prometheus.yml + networks: + - pixels_monitor_network + extra_hosts: + - "host.docker.internal:host-gateway" + + grafana: + image: grafana/grafana:10.1.5 + container_name: pixels-grafana + ports: + - "3000:3000" + volumes: + - grafana-data:/var/lib/grafana + - ./images/grafana-provisioning:/etc/grafana/provisioning + environment: + - GF_SECURITY_ADMIN_USER=admin + - GF_SECURITY_ADMIN_PASSWORD=admin + - GF_AUTH_ANONYMOUS_ENABLED=true + - GF_AUTH_ANONYMOUS_ORG_ROLE=Admin + - GF_DEFAULT_INSTANCE_THEME=light + networks: + - pixels_monitor_network + depends_on: + - prometheus +volumes: + grafana-data: + + +networks: + pixels_monitor_network: + name: pixels_crud_network + driver: bridge diff --git a/develop/example/sql/dss.ri b/develop/example/sql/dss.ri index fb4c002..61b4382 100644 --- a/develop/example/sql/dss.ri +++ b/develop/example/sql/dss.ri @@ -1,100 +1,97 @@ -- Sccsid: @(#)dss.ri 2.1.8.1 --- TPCD Benchmark Version 8.0 +-- TPCH Benchmark Version 8.0 -CONNECT TO TPCD; +--CONNECT TO TPCH; ---ALTER TABLE TPCD.REGION DROP PRIMARY KEY; ---ALTER TABLE TPCD.NATION DROP PRIMARY KEY; ---ALTER TABLE TPCD.PART DROP PRIMARY KEY; ---ALTER TABLE TPCD.SUPPLIER DROP PRIMARY KEY; ---ALTER TABLE TPCD.PARTSUPP DROP PRIMARY KEY; ---ALTER TABLE TPCD.ORDERS DROP PRIMARY KEY; ---ALTER TABLE TPCD.LINEITEM DROP PRIMARY KEY; ---ALTER TABLE TPCD.CUSTOMER DROP PRIMARY KEY; +--ALTER TABLE REGION DROP PRIMARY KEY; +--ALTER TABLE NATION DROP PRIMARY KEY; +--ALTER TABLE PART DROP PRIMARY KEY; +--ALTER TABLE SUPPLIER DROP PRIMARY KEY; +--ALTER TABLE PARTSUPP DROP PRIMARY KEY; +--ALTER TABLE ORDERS DROP PRIMARY KEY; +--ALTER TABLE LINEITEM DROP PRIMARY KEY; +--ALTER TABLE CUSTOMER DROP PRIMARY KEY; -- For table REGION -ALTER TABLE TPCD.REGION +ALTER TABLE REGION ADD PRIMARY KEY (R_REGIONKEY); -- For table NATION -ALTER TABLE TPCD.NATION +ALTER TABLE NATION ADD PRIMARY KEY (N_NATIONKEY); -ALTER TABLE TPCD.NATION -ADD FOREIGN KEY NATION_FK1 (N_REGIONKEY) references TPCD.REGION; +ALTER TABLE NATION +ADD FOREIGN KEY (N_REGIONKEY) references REGION; -COMMIT WORK; +--COMMIT WORK; -- For table PART -ALTER TABLE TPCD.PART +ALTER TABLE PART ADD PRIMARY KEY (P_PARTKEY); -COMMIT WORK; +--COMMIT WORK; -- For table SUPPLIER -ALTER TABLE TPCD.SUPPLIER +ALTER TABLE SUPPLIER ADD PRIMARY KEY (S_SUPPKEY); -ALTER TABLE TPCD.SUPPLIER -ADD FOREIGN KEY SUPPLIER_FK1 (S_NATIONKEY) references TPCD.NATION; +ALTER TABLE SUPPLIER +ADD FOREIGN KEY (S_NATIONKEY) references NATION; -COMMIT WORK; +--COMMIT WORK; -- For table PARTSUPP -ALTER TABLE TPCD.PARTSUPP +ALTER TABLE PARTSUPP ADD PRIMARY KEY (PS_PARTKEY,PS_SUPPKEY); -COMMIT WORK; +--COMMIT WORK; -- For table CUSTOMER -ALTER TABLE TPCD.CUSTOMER +ALTER TABLE CUSTOMER ADD PRIMARY KEY (C_CUSTKEY); -ALTER TABLE TPCD.CUSTOMER -ADD FOREIGN KEY CUSTOMER_FK1 (C_NATIONKEY) references TPCD.NATION; +ALTER TABLE CUSTOMER +ADD FOREIGN KEY (C_NATIONKEY) references NATION; -COMMIT WORK; +--COMMIT WORK; -- For table LINEITEM -ALTER TABLE TPCD.LINEITEM +ALTER TABLE LINEITEM ADD PRIMARY KEY (L_ORDERKEY,L_LINENUMBER); -COMMIT WORK; +--COMMIT WORK; -- For table ORDERS -ALTER TABLE TPCD.ORDERS +ALTER TABLE ORDERS ADD PRIMARY KEY (O_ORDERKEY); -COMMIT WORK; +--COMMIT WORK; -- For table PARTSUPP -ALTER TABLE TPCD.PARTSUPP -ADD FOREIGN KEY PARTSUPP_FK1 (PS_SUPPKEY) references TPCD.SUPPLIER; +ALTER TABLE PARTSUPP +ADD FOREIGN KEY (PS_SUPPKEY) references SUPPLIER; -COMMIT WORK; +--COMMIT WORK; -ALTER TABLE TPCD.PARTSUPP -ADD FOREIGN KEY PARTSUPP_FK2 (PS_PARTKEY) references TPCD.PART; +ALTER TABLE PARTSUPP +ADD FOREIGN KEY (PS_PARTKEY) references PART; -COMMIT WORK; +--COMMIT WORK; -- For table ORDERS -ALTER TABLE TPCD.ORDERS -ADD FOREIGN KEY ORDERS_FK1 (O_CUSTKEY) references TPCD.CUSTOMER; +ALTER TABLE ORDERS +ADD FOREIGN KEY (O_CUSTKEY) references CUSTOMER; -COMMIT WORK; +--COMMIT WORK; -- For table LINEITEM -ALTER TABLE TPCD.LINEITEM -ADD FOREIGN KEY LINEITEM_FK1 (L_ORDERKEY) references TPCD.ORDERS; +ALTER TABLE LINEITEM +ADD FOREIGN KEY (L_ORDERKEY) references ORDERS; -COMMIT WORK; - -ALTER TABLE TPCD.LINEITEM -ADD FOREIGN KEY LINEITEM_FK2 (L_PARTKEY,L_SUPPKEY) references - TPCD.PARTSUPP; - -COMMIT WORK; +--COMMIT WORK; +ALTER TABLE LINEITEM +ADD FOREIGN KEY (L_PARTKEY,L_SUPPKEY) references PARTSUPP; +--COMMIT WORK; \ No newline at end of file diff --git a/develop/example/sql/pg_replica.sql b/develop/example/sql/pg_replica.sql new file mode 100644 index 0000000..96eaf20 --- /dev/null +++ b/develop/example/sql/pg_replica.sql @@ -0,0 +1,15 @@ +ALTER TABLE customer REPLICA IDENTITY USING INDEX customer_pkey; + +ALTER TABLE lineitem REPLICA IDENTITY USING INDEX lineitem_pkey; + +ALTER TABLE nation REPLICA IDENTITY USING INDEX nation_pkey; + +ALTER TABLE orders REPLICA IDENTITY USING INDEX orders_pkey; + +ALTER TABLE part REPLICA IDENTITY USING INDEX part_pkey; + +ALTER TABLE partsupp REPLICA IDENTITY USING INDEX partsupp_pkey; + +ALTER TABLE region REPLICA IDENTITY USING INDEX region_pkey; + +ALTER TABLE supplier REPLICA IDENTITY USING INDEX supplier_pkey; diff --git a/develop/images/grafana-provisioning/dashboards/sink-server.json b/develop/images/grafana-provisioning/dashboards/sink-server.json index 1e622ca..2d609bb 100644 --- a/develop/images/grafana-provisioning/dashboards/sink-server.json +++ b/develop/images/grafana-provisioning/dashboards/sink-server.json @@ -21,7 +21,6 @@ "editable": true, "fiscalYearStartMonth": 0, "graphTooltip": 1, - "id": 1, "links": [], "liveNow": false, "panels": [ @@ -33,6 +32,323 @@ "x": 0, "y": 0 }, + "id": 26, + "panels": [], + "title": "JVM", + "type": "row" + }, + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 9, + "x": 0, + "y": 1 + }, + "id": 27, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "editorMode": "code", + "expr": "jvm_memory_bytes_used{area=\"heap\"}", + "instant": false, + "legendFormat": "__auto", + "range": true, + "refId": "A" + } + ], + "title": "JVM Heap Memory", + "type": "timeseries" + }, + { + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 9 + }, + "id": 25, + "panels": [], + "title": "Debezium", + "type": "row" + }, + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 11, + "w": 5, + "x": 0, + "y": 10 + }, + "id": 23, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "disableTextWrap": false, + "editorMode": "builder", + "expr": "rate(debezium_event_total[1m])", + "fullMetaSearch": false, + "includeNullMetadata": true, + "instant": false, + "legendFormat": "__auto", + "range": true, + "refId": "A", + "useBackend": false + } + ], + "title": "Debezium Event Rate", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 11, + "w": 4, + "x": 5, + "y": 10 + }, + "id": 24, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "disableTextWrap": false, + "editorMode": "builder", + "expr": "rate(row_event_total[$__rate_interval])", + "fullMetaSearch": false, + "includeNullMetadata": true, + "instant": false, + "legendFormat": "__auto", + "range": true, + "refId": "A", + "useBackend": false + } + ], + "title": "Row Event Rate", + "type": "timeseries" + }, + { + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 21 + }, "id": 12, "panels": [], "title": "Basic", @@ -103,7 +419,7 @@ "h": 5, "w": 5, "x": 0, - "y": 1 + "y": 22 }, "id": 9, "options": { @@ -202,7 +518,7 @@ "h": 5, "w": 2, "x": 5, - "y": 1 + "y": 22 }, "id": 10, "options": { @@ -266,7 +582,7 @@ "h": 5, "w": 3, "x": 7, - "y": 1 + "y": 22 }, "id": 11, "options": { @@ -365,7 +681,7 @@ "h": 9, "w": 14, "x": 10, - "y": 1 + "y": 22 }, "id": 4, "options": { @@ -428,7 +744,7 @@ "h": 4, "w": 6, "x": 0, - "y": 6 + "y": 27 }, "id": 8, "options": { @@ -531,7 +847,7 @@ "h": 4, "w": 4, "x": 6, - "y": 6 + "y": 27 }, "id": 2, "options": { @@ -633,7 +949,7 @@ "h": 7, "w": 2, "x": 0, - "y": 10 + "y": 31 }, "id": 18, "options": { @@ -701,7 +1017,7 @@ "h": 7, "w": 4, "x": 2, - "y": 10 + "y": 31 }, "id": 20, "options": { @@ -770,7 +1086,7 @@ "h": 7, "w": 13, "x": 6, - "y": 10 + "y": 31 }, "id": 21, "options": { @@ -837,7 +1153,7 @@ "h": 7, "w": 5, "x": 19, - "y": 10 + "y": 31 }, "id": 6, "options": { @@ -877,7 +1193,7 @@ "h": 1, "w": 24, "x": 0, - "y": 17 + "y": 38 }, "id": 13, "panels": [], @@ -946,7 +1262,7 @@ "h": 9, "w": 4, "x": 0, - "y": 18 + "y": 39 }, "id": 14, "options": { @@ -1044,7 +1360,7 @@ "h": 9, "w": 4, "x": 4, - "y": 18 + "y": 39 }, "id": 15, "options": { @@ -1142,7 +1458,7 @@ "h": 9, "w": 4, "x": 8, - "y": 18 + "y": 39 }, "id": 16, "options": { @@ -1240,7 +1556,7 @@ "h": 9, "w": 6, "x": 12, - "y": 18 + "y": 39 }, "id": 17, "options": { @@ -1372,7 +1688,7 @@ "h": 9, "w": 6, "x": 18, - "y": 18 + "y": 39 }, "id": 22, "options": { @@ -1448,7 +1764,7 @@ "h": 1, "w": 24, "x": 0, - "y": 27 + "y": 48 }, "id": 19, "panels": [], @@ -1456,7 +1772,7 @@ "type": "row" } ], - "refresh": "auto", + "refresh": false, "schemaVersion": 38, "style": "light", "tags": [], @@ -1488,8 +1804,8 @@ ] }, "time": { - "from": "now-5m", - "to": "now" + "from": "2025-10-07T04:53:37.436Z", + "to": "2025-10-07T04:55:21.643Z" }, "timepicker": {}, "timezone": "", diff --git a/develop/images/prometheus/prometheus_local.yml b/develop/images/prometheus/prometheus_local.yml new file mode 100644 index 0000000..340e1a6 --- /dev/null +++ b/develop/images/prometheus/prometheus_local.yml @@ -0,0 +1,9 @@ +global: + scrape_interval: 1s + +scrape_configs: + - job_name: 'pixels-sink' + metrics_path: /metrics + static_configs: + - targets: [ 'host.docker.internal:9464' ] + diff --git a/develop/index.html b/develop/index.html new file mode 100644 index 0000000..7dedf06 --- /dev/null +++ b/develop/index.html @@ -0,0 +1,276 @@ + + + + + Pixels Sink Performance Dashboard + + + + + + +
+ +
+ +
+
+ + + + diff --git a/develop/scripts/install.sh b/develop/scripts/install.sh index 2906792..b741662 100755 --- a/develop/scripts/install.sh +++ b/develop/scripts/install.sh @@ -110,6 +110,7 @@ try_command curl -f -X POST -H "Content-Type: application/json" -d @${CONFIG_DIR check_fatal_exit "Register PostgreSQL Source Connector Fail" if [[ x${enable_tpch} == x"on" && x${load_postgres} == x"on" ]]; then docker exec pixels_postgres_source_db sh -c " psql -Upixels -d pixels_realtime_crud < /example/sql/dss.ddl" + docker exec pixels_postgres_source_db sh -c " psql -Upixels -d pixels_realtime_crud < /example/sql/dss.ri" docker exec pixels_postgres_source_db sh -c " psql -Upixels -d pixels_realtime_crud < /load.sql" fi fi diff --git a/docs/assets/TransactionCoordinator.png b/docs/assets/TransactionCoordinator.png new file mode 100644 index 0000000..8a8ed4b Binary files /dev/null and b/docs/assets/TransactionCoordinator.png differ diff --git a/docs/assets/frame.png b/docs/assets/frame.png new file mode 100644 index 0000000..9d34356 Binary files /dev/null and b/docs/assets/frame.png differ diff --git a/docs/overview.md b/docs/overview.md new file mode 100644 index 0000000..b9b3eb4 --- /dev/null +++ b/docs/overview.md @@ -0,0 +1,180 @@ +# Pixels-Sink Overview + + +![](./assets/frame.png) + + +Pixels Sink目前可以简单分为多层流水线架构,每层之间通过生产者/消费者模式传递数据。 + +## Entry + +介绍Pixels-Sink启动的大致流程 + +### PixelsSinkApp + +目前启动的主类。 + +通过`-c ${filePath}` 传递一个properties文件,该文件会和`${PIXELS_HOME}`(或者pixels默认配置) 下的`pixels.properties`配置文件组合,构成Pixels Sink的配置。 + +配置类位于`PixelsSinkConfig`,目前支持`Integer`、`Long`、`Short`、`Boolean`类型配置的自动解析。 + +### PixelsSinkProvider + +实现了Pixels的SPI, 作为Pixles-Worker启动(没测过)。 + +## Source + +Source层从数据源拉取数据,并进行解析。 + +### Source Input + +| Source Type | 说明 | 相关配置项 | +| ----------- | ---------------------------------------------------------------------------------- | ----- | +| engine | 使用Debezium Engine,直接接到数据库上读取WAL/binlog等 | | +| kafka | 从kafka读取数据 | | +| storage | 利用pixels-storage读取数据。数据格式为sink.proto序列化之后的二进制文件,格式为keyLen + key + valueLen + value | | + +### Source Output + +在Source层,不会做解析操作,而是直接将二进制数据传递给对应的Provider + +## Provider + + +Provider层 + +```mermaid +classDiagram + direction TB + + class EventProvider~SOURCE_RECORD_T, TARGET_RECORD_T~ { + +run() + +close() + +processLoop() + +convertToTargetRecord() + +recordSerdEvent() + +putRawEvent() + +getRawEvent() + +pollRawEvent() + +putTargetEvent() + +getTargetEvent() + } + + class TableEventProvider~SOURCE_RECORD_T~ { + } + class TableEventEngineProvider~T~ { + } + class TableEventKafkaProvider~T~ { + } + class TableEventStorageProvider~T~ { + } + + 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 + +``` + +Provider层通过实现`EventProvider` ,将Source_Record_T 转化为Target_Record_T。 + +例如 + +| Provider | Source Type | Target Type | +| ------------------------------- | --------------- | ----------------------------- | +| TableEventEngineProvider | Debezium Struct | RowChangeEvent | +| TableEventKafkaProvider | kafka topic | RowChangeEvent | +| TableEventStorageProvider | Proto 二进制数据 | RowChangeEvent | +| | | | +| TransactionEventEngineProvider | Debezium Struct | SinkProto.TransactionMetadata | +| TransactionEventKafkaProvider | kafka topic | SinkProto.TransactionMetadata | +| TransactionEventStorageProvider | Proto 二进制数据 | SinkProto.TransactionMetadata | + + +## Processor + +Processor从Provider中拉取数据,写到对应的Retina Writer中。 + +TableProcessor被`TableProviderAndProcessorPipelineManager` 创建,通常每个表对应一个Processor。确保同一个表上记录的顺序。 + +TransactionProcessor只有一个实例。 + +## Writer + +Writer需要实现以下接口 + +| 接口 | 说明 | +| ------------------------------------------------------------- | ------- | +| writeRow(RowChangeEvent rowChangeEvent) | 写行变更 | +| writeTrans(SinkProto.TransactionMetadata transactionMetadata) | 提供事务元信息 | +| flush() | 刷数据 | + +### Retina Writer + +RetinaWriter类实现了PixelsSinkWriter的接口。实现了支持事务的数据回放。 + +#### RetinaServiceProxy + +用于和Retina通信的客户端,持有一个RetinaService。 + +#### Context Manager + +单例,持有所有事务的上下文和TableWriterProxy。 + +决定了什么时候开启事务、什么时候刷数据、怎么提交事务。 + +**分桶** +在处理RowChangeEvent时,做如下处理: ^2906a9 +- **Insert** + - 根据AfterData的Key获取对应Bucket,得到对应TableWriter并写入 +- **Delete** + - 根据BeforeData的Key获取对应Bucket,得到对应TableWriter并写入 +- **Update** + - 如果主键未发生变更,则任意取一个Key,得到对应的Bucket和TableWriter并写入。 + - 如果主键发生了变更,则构造对应的Delete和Insert的RowChangeEvent,这两个Event可能由不同Bucket的TableWriter写入,也可能是同一个Bucket,遵守先Delete再Insert的写入次序。 + +![img.png](./assets/TransactionCoordinator.png) + +#### Table Writer + +每个Table Writer持有多个RetinaClient + +有两个实现: +1. SingleTxWriter, 每次写同一个事务。 +2. CrossTxWriter, 每次发送RPC可能是多个事务。 + +TableWriter写入行变更,(对于Stub,是同步的;对于Stream,在Observer回调中处理)更新对应的SinkContext计数器,如果写入失败,则Rollback事务。 + +#### TransactionProxy + +持有一个TransactionService,提供了同步或异步提交事务的接口。 + +异步提交事务时,TransactionProxy线程在后台进行批量提交。 + + + + +### Proto Writer + +用来造storage source。将数据按照顺序序列化成proto格式。元信息(文件路径等)存在ETCD里 + +### CSV Writer + + +### Flink Writer + diff --git a/perf_freshness.py b/perf_freshness.py new file mode 100644 index 0000000..e5c35ce --- /dev/null +++ b/perf_freshness.py @@ -0,0 +1,147 @@ +import pandas as pd +import matplotlib.pyplot as plt +import numpy as np +import seaborn as sns + +########################################## +# Configuration: CSV Files and Labels +########################################## +csv_files = { + # "10k_2": "resulti7i/10k_freshness.csv", + # "10k": "resulti7i/10k_freshness_2.csv", + # "20k": "resulti7i/20k_freshness.csv", + # "20k": "resulti7i/20k_freshness_2.csv", + # "30k": "resulti7i/30k_freshness_2.csv", + # "40k": "resulti7i/40k_freshness_2.csv", + # "50k": "resulti7i/50k_freshness.csv", + # "60k": "resulti7i/60k_freshness_2.csv", + # "80k": "resulti7i/80k_freshness_2.csv", + # "10k": "resulti7i_100/10k_fresh.csv", + # "20k": "resulti7i_100/20k_fresh.csv", + # # "30k": "resulti7i_100/30k_fresh.csv", + # "40k": "resulti7i_100/40k_fresh.csv", + # "60k": "resulti7i_100/60k_fresh.csv", + "100k": "resulti7i_100/100k_fresh.csv", +} +# csv_files = { +# "Query Transaction": "tmp/i7i_2k_dec_freshness.csv", +# "Query Record": "tmp/i7i_2k_record_dec_freshness.csv", +# "Internal Transaction Context": "tmp/i7i_2k_txn_dec_freshness.csv", +# "Query Selected Table, Trans Mode": "tmp/i7i_2k_batchtest_dec_freshness_2.csv" +# } + +MAX_SECONDS = 1800 # Capture data for the first N seconds +SKIP_SECONDS = 10 # Skip the first N seconds (adjustable) +BIN_SECONDS = 180 # Average window (seconds) +MAX_FRESHNESS = 500000 # Filter out useless data during initial warmup +########################################## +# Data Loading and Processing +########################################## +data = {} +for label, path in csv_files.items(): + df = pd.read_csv(path, header=None, names=["ts", "freshness"]) + + # Convert to datetime + df["ts"] = pd.to_datetime(df["ts"], unit="ms") + + # Relative seconds + t0 = df["ts"].iloc[0] + df["sec"] = (df["ts"] - t0).dt.total_seconds() + + # Skip initial SKIP_SECONDS + df = df[df["sec"] >= SKIP_SECONDS] + + # Filter by max freshness threshold + df = df[df["freshness"] <= MAX_FRESHNESS] + + # Recalculate time (align all curves to start at 0 seconds) + t_new0 = df["ts"].iloc[0] + df["sec"] = (df["ts"] - t_new0).dt.total_seconds() + + # Limit to MAX_SECONDS + df = df[df["sec"] <= MAX_SECONDS] + + # Sample using an adjustable averaging window + df_bin = df.resample(f"{BIN_SECONDS}s", on="ts").mean().reset_index() + + # Align horizontal axis (time series) + df_bin["bin_sec"] = (df_bin["ts"] - df_bin["ts"].iloc[0]).dt.total_seconds() + + data[label] = df_bin + + +########################################## +# Plot 1: Smoothed/Beautified Time Series Oscillations +########################################## +# Set overall style; whitegrid looks clean and professional +sns.set_theme(style="whitegrid") + +plt.figure(figsize=(12, 6)) # Slightly wider for better time trend visualization + +for label, df in data.items(): + # Ensure data is sorted + df_plot = df.sort_values("bin_sec") + + # Option A: Increase line width and anti-aliasing; use alpha for transparency to distinguish overlaps + line, = plt.plot( + df_plot["bin_sec"], + df_plot["freshness"], + label=label, + linewidth=1.8, + alpha=0.9, + antialiased=True + ) + + +# Axis labeling and beautification +plt.xlabel("Time (sec)", fontsize=11, fontweight='bold') +plt.ylabel(f"Freshness (ms, {BIN_SECONDS}s average)", fontsize=11, fontweight='bold') + +# Remove top and right spines for a cleaner look +sns.despine() + +plt.title( + f"Freshness Oscillations\n({BIN_SECONDS}s Binning, Skip {SKIP_SECONDS}s)", + fontsize=13, + pad=15 +) + +# Move legend outside or to the top right to avoid blocking curves +plt.legend(bbox_to_anchor=(1.05, 1), loc='upper left', borderaxespad=0.) + +plt.grid(True, which="major", ls="-", alpha=0.4) +plt.tight_layout() +plt.savefig("freshness_over_time_smooth.png", dpi=300) # Save with high resolution +plt.close() + + +########################################## +# Plot 2: Inverted CDF (X-axis 0-1, Step 0.1) +########################################## +plt.figure(figsize=(10, 5)) + +for label, df in data.items(): + vals = np.sort(df["freshness"].dropna()) + prob = np.linspace(0, 1, len(vals)) + + # X-axis is probability [0, 1], Y-axis is value + plt.plot(prob, vals, label=label) + +# Set X-axis ticks: from 0 to 1.1 (excluding 1.1) with step 0.1 +plt.xticks(np.arange(0, 1.1, 0.1)) +plt.xlim(0, 1) # Force display range between 0 and 1 + +# plt.yscale("log") +plt.xlabel("CDF (Probability)") +plt.ylabel(f"Freshness (ms, {BIN_SECONDS}s average)") +plt.title( + f"Inverted Freshness CDF ({BIN_SECONDS}-Second Sampled, Skip {SKIP_SECONDS}s)" +) + +plt.grid(True, which="both", ls="-", alpha=0.3) +plt.legend() +plt.tight_layout() +plt.savefig("freshness_cdf_fixed_ticks.png") +plt.close() + +print("Plots generated: freshness_over_time_smooth.png, freshness_cdf_fixed_ticks.png") \ No newline at end of file diff --git a/perf_rate.py b/perf_rate.py new file mode 100644 index 0000000..797cce5 --- /dev/null +++ b/perf_rate.py @@ -0,0 +1,116 @@ +import pandas as pd +import matplotlib.pyplot as plt +import numpy as np +from datetime import datetime, date + +########################################## +# Configuration: CSV Files and Labels +########################################## +csv_files = { + # "10k": "resulti7i/10k_rate_2.csv", + # "20k": "resulti7i/20k_rate_2.csv", + # "30k": "resulti7i/30k_rate_2.csv", + # "40k": "resulti7i/40k_rate_2.csv", + # "50k": "resulti7i/50k_rate.csv", + # "60k": "resulti7i/60k_rate_2.csv", + # "80k": "resulti7i/80k_rate_2.csv", + "100k": "resulti7i_100/100k_rate.csv", +} + +COL_NAMES = ["time", "rows", "txns", "debezium", "serdRows", "serdTxs"] +PLOT_COL = "rows" + +MAX_SECONDS = 1800 +SKIP_SECONDS = 10 +BIN_SECONDS = 60 + +########################################## +# Data Loading and Processing +########################################## +data = {} +for label, path in csv_files.items(): + print(f"Processing: {label} -> {path}") + + # 1. Load data + df = pd.read_csv(path, header=None, names=COL_NAMES, sep=',') + + # 2. Handle timestamps and skip rows with incorrect formats + # errors='coerce' turns unparseable formats into NaT + df["ts"] = pd.to_datetime(df["time"], format="%H:%M:%S", errors='coerce') + + # Remove rows where time could not be parsed (NaT) + initial_count = len(df) + df = df.dropna(subset=["ts"]).copy() + if len(df) < initial_count: + print(f" Note: Skipped {initial_count - len(df)} rows with incorrect data format") + + # Combine with current date + df["ts"] = df["ts"].dt.time.apply(lambda x: datetime.combine(date.today(), x)) + + # 3. Calculate relative time + df = df.sort_values("ts") # Ensure time is ordered + t0 = df["ts"].iloc[0] + df["sec"] = (df["ts"] - t0).dt.total_seconds() + + # 4. Filter time range + df = df[df["sec"] >= SKIP_SECONDS].copy() + if df.empty: + print(f" Warning: {label} has no data remaining after skipping {SKIP_SECONDS}s") + continue + + t_new0 = df["ts"].iloc[0] + df["sec"] = (df["ts"] - t_new0).dt.total_seconds() + df = df[df["sec"] <= MAX_SECONDS] + + # 5. Resampling and Aggregation + # Ensure numeric columns are numeric types before setting index + # This prevents mean() failures if strings are mixed in numeric columns + for col in ["rows", "txns", "debezium", "serdRows", "serdTxs"]: + df[col] = pd.to_numeric(df[col], errors='coerce') + + df = df.set_index("ts") + + # Perform mean calculation on numeric columns only, ignoring strings (like 'time' column) + df_bin = df.resample(f"{BIN_SECONDS}s").mean(numeric_only=True).reset_index() + + # 6. Align horizontal axis + if not df_bin.empty: + df_bin["bin_sec"] = (df_bin["ts"] - df_bin["ts"].iloc[0]).dt.total_seconds() + data[label] = df_bin + +########################################## +# Plot 1: Time Series Fluctuations +########################################## +plt.figure(figsize=(10, 5)) +for label, df in data.items(): + plt.plot(df["bin_sec"], df[PLOT_COL], label=label) + +plt.xlabel("Time (sec)") +plt.ylabel(f"{PLOT_COL} ({BIN_SECONDS}s average)") +plt.title(f"{PLOT_COL} Over Time ({BIN_SECONDS}s Avg)") +plt.legend() +plt.grid(True, linestyle='--', alpha=0.7) +plt.tight_layout() +plt.savefig(f"rate_{PLOT_COL}_over_time_variable_bin.png") +plt.close() + +########################################## +# Plot 2: CDF (Cumulative Distribution Function) +########################################## +plt.figure(figsize=(10, 5)) +for label, df in data.items(): + vals = np.sort(df[PLOT_COL].dropna()) + if len(vals) > 0: + y = np.linspace(0, 1, len(vals)) + plt.plot(vals, y, label=label) + +plt.xlabel(f"{PLOT_COL} ({BIN_SECONDS}s average)") +plt.ylabel("CDF") +plt.title(f"{PLOT_COL} CDF Distribution") +plt.legend() +plt.grid(True, linestyle='--', alpha=0.7) +plt.tight_layout() +plt.savefig(f"rate_{PLOT_COL}_cdf_variable_bin.png") +plt.close() + +print(f"\nAll tasks completed! Plots have been generated.") \ No newline at end of file diff --git a/perf_web_monitor.py b/perf_web_monitor.py new file mode 100644 index 0000000..d8f5caf --- /dev/null +++ b/perf_web_monitor.py @@ -0,0 +1,82 @@ +from flask import Flask, render_template, jsonify, request, abort +import pandas as pd +import os +from functools import lru_cache +from time import time + +# Configuration +DATA_DIR = "/home/ubuntu/pixels-sink/resulti7i_100" +# DATA_DIR = "/home/antio2/projects/pixels-sink/tmp" +PORT = 8083 +CACHE_TTL = 5 # seconds + +app = Flask(__name__, template_folder='develop') + +# ---- 1. File Reading Cache (Reduces frequent I/O) ---- +@lru_cache(maxsize=64) +def _read_csv_cached(path, mtime): + """Read CSV with simple caching by modification time.""" + df = pd.read_csv(path, names=["time", "rows", "txns", "debezium", "serdRows", "serdTxs"]) + # Take only the last 300 records for real-time display + df = df.tail(300) + + # Drop rows where all numeric values are zero + mask = (df.drop(columns=["time"]).sum(axis=1) != 0) + df = df.loc[mask] + + return {col: df[col].tolist() for col in df.columns} + +def read_csv_with_cache(path): + """Wrapper that invalidates cache when file modification time changes.""" + try: + mtime = os.path.getmtime(path) + return _read_csv_cached(path, mtime) + except Exception as e: + print(f"[WARN] Failed to read {path}: {e}") + return None + +# ---- 2. Main Index Page ---- +@app.route('/') +def index(): + return render_template('index.html') + +# ---- 3. CSV File List ---- +@app.route('/list') +def list_csv(): + """Returns a list of all CSV files in the data directory.""" + try: + files = [ + f for f in os.listdir(DATA_DIR) + if f.endswith(".csv") and os.path.isfile(os.path.join(DATA_DIR, f)) + ] + return jsonify(sorted(files)) + except Exception as e: + print(f"[ERROR] list_csv failed: {e}") + return jsonify([]) + +# ---- 4. Data Retrieval for Specific File ---- +@app.route('/data/') +def get_data_file(filename): + print(f"Requesting data for: {filename}") + path = os.path.join(DATA_DIR, filename) + + if not os.path.exists(path): + abort(404, description=f"File {filename} not found") + + data = read_csv_with_cache(path) + if not data: + print("File is empty or could not be processed") + return jsonify({}) # Return empty object if file processing fails + return jsonify(data) + +# ---- 5. Simple Health Check ---- +@app.route('/health') +def health(): + return jsonify({"status": "ok", "time": time()}) + +# ---- 6. Server Startup ---- +if __name__ == '__main__': + # Ensure the data directory exists + os.makedirs(DATA_DIR, exist_ok=True) + # Run server on all interfaces at the configured port + app.run(host='0.0.0.0', port=PORT, debug=False) \ No newline at end of file diff --git a/pixels-sink b/pixels-sink new file mode 100755 index 0000000..fed1eba --- /dev/null +++ b/pixels-sink @@ -0,0 +1,48 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Resolve the absolute path of this script +SOURCE_PATH=$(readlink -f "$BASH_SOURCE") 2>/dev/null +SINK_DIR=$(dirname "$SOURCE_PATH") + +# Environment variable +# export PIXELS_HOME="/home/ubuntu/opt/pixels" + +# Application properties file +#PROPERTIES_FILE="/home/ubuntu/pixels-sink/src/main/resources/pixels-sink.aws.properties" +#PROPERTIES_FILE="/home/pixels/projects/pixels-sink/src/main/resources/pixels-sink.local.properties" +#PROPERTIES_FILE="${SINK_DIR}/conf/pixels-sink.pg.properties" +PROPERTIES_FILE="${SINK_DIR}/conf/pixels-sink.aws.properties" + +# JVM config file +JVM_CONFIG_FILE="${SINK_DIR}/conf/jvm.conf" + +if [[ ! -f "$JVM_CONFIG_FILE" ]]; then + echo "JVM config file not found: $JVM_CONFIG_FILE" + exit 1 +fi + +# Read JVM options (ignore comments and empty lines) +JVM_OPTS=$(grep -v '^\s*#' "$JVM_CONFIG_FILE" | grep -v '^\s*$' | xargs) + +# Main class (for reference, though fat-jar specifies it in MANIFEST) +MAIN_CLASS="io.pixelsdb.pixels.sink.PixelsSinkApp" + +# Application arguments +APP_ARGS="-c $PROPERTIES_FILE" + +# Path to the fat jar +APP_JAR="$SINK_DIR/target/pixels-sink-0.2.0-SNAPSHOT-full.jar" + +if [[ ! -f "$APP_JAR" ]]; then + echo "Application jar not found: $APP_JAR" + exit 1 +fi + +echo "Starting PixelsSinkApp..." +echo "PIXELS_HOME = $PIXELS_HOME" +echo "JVM_OPTS = $JVM_OPTS" +echo "APP_JAR = $APP_JAR" +echo "APP_ARGS = $APP_ARGS" + +exec java $JVM_OPTS -jar "$APP_JAR" $APP_ARGS diff --git a/pom.xml b/pom.xml index e9896e7..a4919eb 100644 --- a/pom.xml +++ b/pom.xml @@ -36,9 +36,10 @@ 0.9.0 3.8.0 5.8 - 1.18.36 - 3.0.7.Final + 1.18.42 + 3.2.3.Final 1.4.13 + 440 @@ -52,13 +53,36 @@ pixels-core true + + io.pixelsdb + pixels-retina + true + test + + + io.etcd + jetcd-core + true + + + io.netty + netty-all + + + io.grpc + grpc-netty + + + io.trino + trino-jdbc + ${trino.version} + com.alibaba fastjson true - org.apache.logging.log4j log4j-core @@ -111,12 +135,22 @@ kafka-clients ${dep.kafka.version} + + org.apache.kafka + connect-api + ${dep.kafka.version} + com.opencsv opencsv ${dep.opencsv.version} + + com.google.guava + guava + 33.2.0-jre + org.projectlombok @@ -142,7 +176,11 @@ debezium-sink ${dep.debezium.version} - + + io.debezium + debezium-connector-postgres + ${dep.debezium.version} + junit @@ -182,13 +220,17 @@ 2.6.2.Final - io.prometheus simpleclient 0.16.0 + + io.prometheus + simpleclient_hotspot + 0.16.0 + io.prometheus @@ -196,6 +238,18 @@ 0.16.0 + + org.apache.commons + commons-math3 + 3.6.1 + + + + + io.pixelsdb + pixels-storage-localfs + + @@ -257,43 +311,24 @@ develop/images/pixels-sink/Dockerfile - - com.github.os72 - protoc-jar-maven-plugin - 3.3.0.1 - - - generate-sources - - run - - - - src/main/proto - - - - java - src/main/java - - - grpc-java - src/main/java - io.grpc:protoc-gen-grpc-java:1.53.0 - - - - - - - - org.apache.maven.plugins - maven-compiler-plugin - - 16 - 16 - - + + org.apache.maven.plugins + maven-compiler-plugin + + 17 + 17 + + + org.projectlombok + lombok + ${dep.lombok.version} + + + + -parameters + + + \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..8cb1b70 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,4 @@ +flask +pandas +matplotlib +numpy diff --git a/src/main/java/io/pixelsdb/pixels/sink/PixelsSinkApp.java b/src/main/java/io/pixelsdb/pixels/sink/PixelsSinkApp.java index 92f3632..f6329d7 100644 --- a/src/main/java/io/pixelsdb/pixels/sink/PixelsSinkApp.java +++ b/src/main/java/io/pixelsdb/pixels/sink/PixelsSinkApp.java @@ -1,26 +1,38 @@ /* * 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 + * This file is part of Pixels. * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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. * - * 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. + * 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; -import io.pixelsdb.pixels.sink.concurrent.TransactionCoordinatorFactory; import io.pixelsdb.pixels.sink.config.CommandLineConfig; +import io.pixelsdb.pixels.sink.config.PixelsSinkConfig; import io.pixelsdb.pixels.sink.config.factory.PixelsSinkConfigFactory; -import io.pixelsdb.pixels.sink.monitor.MetricsFacade; -import io.pixelsdb.pixels.sink.monitor.SinkMonitor; +import io.pixelsdb.pixels.sink.freshness.FreshnessClient; +import io.pixelsdb.pixels.sink.source.SinkSource; +import io.pixelsdb.pixels.sink.source.SinkSourceFactory; +import io.pixelsdb.pixels.sink.util.MetricsFacade; +import io.pixelsdb.pixels.sink.writer.PixelsSinkWriter; +import io.pixelsdb.pixels.sink.writer.PixelsSinkWriterFactory; +import io.pixelsdb.pixels.sink.writer.retina.SinkContextManager; +import io.pixelsdb.pixels.sink.writer.retina.TransactionProxy; +import io.prometheus.client.exporter.HTTPServer; +import io.prometheus.client.hotspot.DefaultExports; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -31,22 +43,59 @@ */ public class PixelsSinkApp { private static final Logger LOGGER = LoggerFactory.getLogger(PixelsSinkApp.class); - private static SinkMonitor sinkMonitor = new SinkMonitor(); + private static SinkSource sinkSource; + private static HTTPServer prometheusHttpServer; + private static FreshnessClient freshnessClient; public static void main(String[] args) throws IOException { - Runtime.getRuntime().addShutdownHook(new Thread(() -> { - sinkMonitor.stopMonitor(); - TransactionCoordinatorFactory.reset(); + Runtime.getRuntime().addShutdownHook(new Thread(() -> + { + PixelsSinkConfig config = PixelsSinkConfigFactory.getInstance(); + TransactionProxy.staticClose(); + sinkSource.stopProcessor(); LOGGER.info("Pixels Sink Server shutdown complete"); + if (config.getSinkMonitorFreshnessLevel().equals("embed") && freshnessClient != null) { + freshnessClient.stop(); + } + if (prometheusHttpServer != null) { + prometheusHttpServer.close(); + } + MetricsFacade.getInstance().stop(); + PixelsSinkWriter pixelsSinkWriter = PixelsSinkWriterFactory.getWriter(); + if (pixelsSinkWriter != null) { + try { + pixelsSinkWriter.close(); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + })); init(args); - sinkMonitor.startSinkMonitor(); + PixelsSinkConfig config = PixelsSinkConfigFactory.getInstance(); + sinkSource = SinkSourceFactory.createSinkSource(); + + try { + if (config.isMonitorEnabled()) { + DefaultExports.initialize(); + prometheusHttpServer = new HTTPServer(config.getMonitorPort()); + } + } catch (IOException e) { + throw new RuntimeException(e); + } + + if (config.getSinkMonitorFreshnessLevel().equals("embed")) { + freshnessClient = FreshnessClient.getInstance(); + freshnessClient.start(); + } + sinkSource.start(); } private static void init(String[] args) throws IOException { CommandLineConfig cmdLineConfig = new CommandLineConfig(args); PixelsSinkConfigFactory.initialize(cmdLineConfig.getConfigPath()); - MetricsFacade.initialize(); + MetricsFacade metricsFacade = MetricsFacade.getInstance(); + metricsFacade.setSinkContextManager(SinkContextManager.getInstance()); } } diff --git a/src/main/java/io/pixelsdb/pixels/sink/PixelsSinkProvider.java b/src/main/java/io/pixelsdb/pixels/sink/PixelsSinkProvider.java index f7bdd71..15749c4 100644 --- a/src/main/java/io/pixelsdb/pixels/sink/PixelsSinkProvider.java +++ b/src/main/java/io/pixelsdb/pixels/sink/PixelsSinkProvider.java @@ -1,18 +1,21 @@ /* * 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 + * This file is part of Pixels. * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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. * - * 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. + * 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; @@ -20,26 +23,27 @@ import io.pixelsdb.pixels.common.sink.SinkProvider; import io.pixelsdb.pixels.common.utils.ConfigFactory; import io.pixelsdb.pixels.sink.config.factory.PixelsSinkConfigFactory; -import io.pixelsdb.pixels.sink.monitor.MetricsFacade; -import io.pixelsdb.pixels.sink.monitor.SinkMonitor; +import io.pixelsdb.pixels.sink.source.SinkSource; +import io.pixelsdb.pixels.sink.source.SinkSourceFactory; +import io.pixelsdb.pixels.sink.util.MetricsFacade; public class PixelsSinkProvider implements SinkProvider { - private SinkMonitor sinkMonitor; + private SinkSource sinkSource; public void start(ConfigFactory config) { PixelsSinkConfigFactory.initialize(config); - MetricsFacade.initialize(); - sinkMonitor = new SinkMonitor(); - sinkMonitor.startSinkMonitor(); + MetricsFacade.getInstance(); + sinkSource = SinkSourceFactory.createSinkSource(); + sinkSource.start(); } @Override public void shutdown() { - sinkMonitor.stopMonitor(); + sinkSource.stopProcessor(); } @Override public boolean isRunning() { - return sinkMonitor.isRunning(); + return sinkSource.isRunning(); } } diff --git a/src/main/java/io/pixelsdb/pixels/sink/concurrent/SinkContext.java b/src/main/java/io/pixelsdb/pixels/sink/concurrent/SinkContext.java deleted file mode 100644 index b30f350..0000000 --- a/src/main/java/io/pixelsdb/pixels/sink/concurrent/SinkContext.java +++ /dev/null @@ -1,106 +0,0 @@ -/* - * 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.concurrent; - -import io.pixelsdb.pixels.common.transaction.TransContext; -import io.pixelsdb.pixels.sink.SinkProto; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.util.Map; -import java.util.Set; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.atomic.AtomicInteger; -import java.util.concurrent.locks.Condition; -import java.util.concurrent.locks.ReentrantLock; - -class SinkContext { - private static final Logger LOGGER = LoggerFactory.getLogger(SinkContext.class); - final ReentrantLock lock = new ReentrantLock(); - final Condition cond = lock.newCondition(); - - final String sourceTxId; - final Map tableCursors = new ConcurrentHashMap<>(); - final Map tableCounters = new ConcurrentHashMap<>(); - final AtomicInteger pendingEvents = new AtomicInteger(0); - final CompletableFuture completionFuture = new CompletableFuture<>(); - TransContext pixelsTransCtx; - volatile boolean completed = false; - - - SinkContext(String sourceTxId) { - this.sourceTxId = sourceTxId; - this.pixelsTransCtx = null; - } - - SinkContext(String sourceTxId, TransContext pixelsTransCtx) { - this.sourceTxId = sourceTxId; - this.pixelsTransCtx = pixelsTransCtx; - } - - boolean isReadyForDispatch(String table, long collectionOrder) { - lock.lock(); - boolean ready = tableCursors - .computeIfAbsent(table, k -> 1L) >= collectionOrder; - lock.unlock(); - return ready; - } - - void updateCursor(String table, long currentOrder) { - tableCursors.compute(table, (k, v) -> - (v == null) ? currentOrder + 1 : Math.max(v, currentOrder + 1)); - } - - void updateCounter(String table) { - tableCounters.compute(table, (k, v) -> - (v == null) ? 1 : v + 1); - } - - Set getTrackedTables() { - return tableCursors.keySet(); - } - - boolean isCompleted(SinkProto.TransactionMetadata tx) { - for (SinkProto.DataCollection dataCollection : tx.getDataCollectionsList()) { - // Long targetEventCount = tableCursors.get(dataCollection.getDataCollection()); - Long targetEventCount = tableCounters.get(dataCollection.getDataCollection()); - long target = targetEventCount == null ? 0 : targetEventCount; - LOGGER.debug("TX {}, Table {}, event count {}, tableCursors {}", tx.getId(), dataCollection.getDataCollection(), dataCollection.getEventCount(), target); - if (dataCollection.getEventCount() > target) { - return false; - } - } - return true; - } - - boolean isExpired() { - // TODO: expire timeout transaction - return false; - // return System.currentTimeMillis() - pixelsTransCtx.getTimestamp() > TX_TIMEOUT_MS; - } - - void markCompleted() { - this.completed = true; - } - - void awaitCompletion() throws InterruptedException, ExecutionException { - completionFuture.get(); - } -} diff --git a/src/main/java/io/pixelsdb/pixels/sink/concurrent/TransactionCoordinator.java b/src/main/java/io/pixelsdb/pixels/sink/concurrent/TransactionCoordinator.java deleted file mode 100644 index d5412a0..0000000 --- a/src/main/java/io/pixelsdb/pixels/sink/concurrent/TransactionCoordinator.java +++ /dev/null @@ -1,449 +0,0 @@ -/* - * 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.concurrent; - -import io.pixelsdb.pixels.common.exception.TransException; -import io.pixelsdb.pixels.common.transaction.TransContext; -import io.pixelsdb.pixels.common.transaction.TransService; -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.monitor.MetricsFacade; -import io.pixelsdb.pixels.sink.sink.PixelsSinkWriter; -import io.pixelsdb.pixels.sink.sink.PixelsSinkWriterFactory; -import io.pixelsdb.pixels.sink.util.LatencySimulator; -import io.prometheus.client.Summary; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.io.IOException; -import java.util.Comparator; -import java.util.List; -import java.util.concurrent.*; -import java.util.stream.Collectors; - -public class TransactionCoordinator { - private static final Logger LOGGER = LoggerFactory.getLogger(TransactionCoordinator.class); - public static final int INITIAL_CAPACITY = 11; - private final PixelsSinkWriter writer; - - final ConcurrentMap activeTxContexts = new ConcurrentHashMap<>(); - final ExecutorService dispatchExecutor = Executors.newCachedThreadPool(); - private final ExecutorService transactionExecutor = Executors.newCachedThreadPool(); - private final ConcurrentMap> orphanedEvents = new ConcurrentHashMap<>(); - private final ConcurrentMap> orderedBuffers = new ConcurrentHashMap<>(); - // private final BlockingQueue nonTxQueue = new LinkedBlockingQueue<>(); - private long TX_TIMEOUT_MS = PixelsSinkConfigFactory.getInstance().getTransactionTimeout(); - private final ScheduledExecutorService timeoutScheduler = - Executors.newSingleThreadScheduledExecutor(); - - private final TransactionManager transactionManager = TransactionManager.Instance(); - private final TransService transService; - - private final MetricsFacade metricsFacade = MetricsFacade.getInstance(); - private final PixelsSinkConfig pixelsSinkConfig = PixelsSinkConfigFactory.getInstance(); - - - TransactionCoordinator() { - try { - this.writer = PixelsSinkWriterFactory.getWriter(); - } catch (IOException e) { - throw new RuntimeException(e); - } - transService = TransService.Instance(); - // startDispatchWorker(); - startTimeoutChecker(); - } - - public void processTransactionEvent(SinkProto.TransactionMetadata txMeta) { - if (txMeta.getStatus() == SinkProto.TransactionStatus.BEGIN) { - handleTxBegin(txMeta); - } else if (txMeta.getStatus() == SinkProto.TransactionStatus.END) { - handleTxEnd(txMeta); - metricsFacade.recordTransaction(); - } - } - - public void processRowEvent(RowChangeEvent event) { - if (event == null) { - return; - } - - metricsFacade.recordRowChange(event.getTable(), event.getOp()); - event.startLatencyTimer(); - if (event.getTransaction() == null || event.getTransaction().getId().isEmpty()) { - handleNonTxEvent(event); - return; - } - - String txId = event.getTransaction().getId(); - String table = event.getFullTableName(); - - long collectionOrder = event.getTransaction().getDataCollectionOrder(); - long totalOrder = event.getTransaction().getTotalOrder(); - - LOGGER.debug("Receive event {} {}/{} {}/{} ", event.getOp().toString(), txId, totalOrder, table, collectionOrder); - SinkContext ctx = activeTxContexts.get(txId); - if (ctx == null) { - // async method - // try { - // ctx = startTrans(txId).get(); - // } catch (InterruptedException | ExecutionException e) { - // throw new RuntimeException(e); - // } - - // sync mode: we should wait for transaction message - bufferOrphanedEvent(event); - return; - } - ctx.lock.lock(); - try { - ctx.cond.signalAll(); - } finally { - ctx.lock.unlock(); - } - - OrderedEvent orderedEvent = new OrderedEvent(event, collectionOrder, totalOrder); -// if (ctx.isReadyForDispatch(table, collectionOrder)) { - if(true) { - LOGGER.debug("Immediately dispatch {} {}/{}", event.getTransaction().getId(), collectionOrder, totalOrder); - ctx.pendingEvents.incrementAndGet(); - dispatchImmediately(event, ctx); - // ctx.updateCursor(table, collectionOrder); - ctx.updateCounter(table); - checkPendingEvents(ctx, table); - } else { - bufferOrderedEvent(ctx, orderedEvent); - } - } - - private void handleTxBegin(SinkProto.TransactionMetadata txBegin) { - // startTrans(txBegin.getId()).get(); - startTransSync(txBegin.getId()); - } - - private void startTransSync(String sourceTxId) { - SinkContext ctx = activeTxContexts.computeIfAbsent(sourceTxId, k -> new SinkContext(sourceTxId)); - TransContext pixelsTransContext; - Summary.Timer transLatencyTimer = metricsFacade.startTransLatencyTimer(); - if (pixelsSinkConfig.isRpcEnable()) { - pixelsTransContext = transactionManager.getTransContext(); - } else { - LatencySimulator.smartDelay(); - pixelsTransContext = new TransContext(sourceTxId.hashCode(), System.currentTimeMillis(), false); - } - transLatencyTimer.close(); - ctx.pixelsTransCtx = pixelsTransContext; - List buffered = getBufferedEvents(sourceTxId); - if (buffered != null) { - buffered.forEach(be -> processBufferedEvent(ctx, be)); - } - LOGGER.info("Begin Tx Sync: {}", sourceTxId); - } - - @Deprecated - private Future startTrans(String sourceTxId) { - SinkContext ctx = activeTxContexts.computeIfAbsent(sourceTxId, k -> new SinkContext(sourceTxId)); - return transactionExecutor.submit(() -> { - try { - ctx.lock.lock(); - TransContext pixelsTransContext; - Summary.Timer transLatencyTimer = metricsFacade.startTransLatencyTimer(); - if (pixelsSinkConfig.isRpcEnable()) { - pixelsTransContext = transService.beginTrans(false); - } else { - LatencySimulator.smartDelay(); - pixelsTransContext = new TransContext(sourceTxId.hashCode(), System.currentTimeMillis(), false); - } - transLatencyTimer.close(); - activeTxContexts.get(sourceTxId).pixelsTransCtx = pixelsTransContext; - ctx.lock.unlock(); - List buffered = getBufferedEvents(sourceTxId); - if (buffered != null) { - buffered - // .stream() - // .sorted(Comparator.comparingLong(BufferedEvent::getTotalOrder)) - .forEach(be -> processBufferedEvent(ctx, be)); - } - } catch (TransException e) { - throw new RuntimeException(e); - } - LOGGER.info("Begin Tx: {}", sourceTxId); - return ctx; - }); - } - - private void handleTxEnd(SinkProto.TransactionMetadata txEnd) { - String txId = txEnd.getId(); - SinkContext ctx = activeTxContexts.get(txId); - transactionExecutor.submit(() -> { - LOGGER.info("Begin to Commit transaction: {}, total event {}; Data Collection {}", txId, txEnd.getEventCount(), - txEnd.getDataCollectionsList().stream() - .map(dc -> dc.getDataCollection() + "=" + - ctx.tableCursors.getOrDefault(dc.getDataCollection(), 0L) + - "/" + dc.getEventCount()) - .collect(Collectors.joining(", "))); - if (ctx != null) { - try { - ctx.lock.lock(); - ctx.markCompleted(); - try { - while (!ctx.isCompleted(txEnd)) { - ctx.lock.lock(); - LOGGER.debug("TX End Get Lock {}", txId); - LOGGER.debug("Waiting for events in TX {}: {}", txId, - txEnd.getDataCollectionsList().stream() - .map(dc -> dc.getDataCollection() + "=" + - ctx.tableCursors.getOrDefault(dc.getDataCollection(), 0L) + - "/" + dc.getEventCount()) - .collect(Collectors.joining(", "))); - - ctx.cond.await(100, TimeUnit.MILLISECONDS); - } - } finally { - ctx.lock.unlock(); - } - - if (ctx.pendingEvents.get() > 0) { - LOGGER.info("Waiting for {} pending events in TX {}", - ctx.pendingEvents.get(), txId); - ctx.awaitCompletion(); - } - - flushRemainingEvents(ctx); - activeTxContexts.remove(txId); - LOGGER.info("Committed transaction: {}", txId); - Summary.Timer transLatencyTimer = metricsFacade.startTransLatencyTimer(); - if (pixelsSinkConfig.isRpcEnable()) { - transService.commitTrans(ctx.pixelsTransCtx.getTransId(), ctx.pixelsTransCtx.getTimestamp()); - } else { - LatencySimulator.smartDelay(); - } - transLatencyTimer.close(); - } catch (InterruptedException | ExecutionException | TransException e) { - // TODO(AntiO2) abort? - LOGGER.error("Failed to commit transaction {}", txId, e); - } - } - } - ); - } - - - private void bufferOrphanedEvent(RowChangeEvent event) { - orphanedEvents.computeIfAbsent(event.getTransaction().getId(), k -> new CopyOnWriteArrayList<>()).add(event); - // LOGGER.debug("Buffered orphan event for TX {}: {}/{}", txId, event.collectionOrder, event.totalOrder); - } - - private List getBufferedEvents(String txId) { - return orphanedEvents.remove(txId); - } - - private void processBufferedEvent(SinkContext ctx, RowChangeEvent bufferedEvent) { - String table = bufferedEvent.getTable(); - dispatchImmediately(bufferedEvent, ctx); - -// long collectionOrder = bufferedEvent.collectionOrder; -// if (ctx.isReadyForDispatch(table, collectionOrder)) { -// dispatchImmediately(bufferedEvent.event, ctx); -// ctx.lock.lock(); -// ctx.updateCursor(table, collectionOrder); -// ctx.lock.unlock(); -// checkPendingEvents(ctx, table); -// } else { -// bufferOrderedEvent(ctx, new OrderedEvent( -// bufferedEvent.event, -// collectionOrder, -// bufferedEvent.totalOrder -// )); -// ctx.pendingEvents.incrementAndGet(); // track pending events -// } - } - - private void bufferOrderedEvent(SinkContext ctx, OrderedEvent event) { - String bufferKey = ctx.sourceTxId + "|" + event.getTable(); - LOGGER.info("Buffered out-of-order event: {} {}/{}. Pending Events: {}", - bufferKey, event.collectionOrder, event.totalOrder, ctx.pendingEvents.incrementAndGet()); - orderedBuffers.computeIfAbsent(bufferKey, k -> - new PriorityBlockingQueue<>(INITIAL_CAPACITY, Comparator.comparingLong(OrderedEvent::getCollectionOrder)) - ).offer(event); - } - - private void checkPendingEvents(SinkContext ctx, String table) { - String bufferKey = ctx.sourceTxId + "|" + table; - PriorityBlockingQueue buffer = orderedBuffers.get(bufferKey); - if (buffer == null) return; - - while (!buffer.isEmpty()) { - OrderedEvent nextEvent = buffer.peek(); - if (ctx.isReadyForDispatch(table, nextEvent.collectionOrder)) { - LOGGER.debug("Ordered buffer dispatch {} {}/{}", bufferKey, nextEvent.collectionOrder, nextEvent.totalOrder); - dispatchImmediately(nextEvent.event, ctx); - buffer.poll(); - } else { - break; - } - } - } - - private void startDispatchWorker() { -// dispatchExecutor.execute(() -> { -// while (!Thread.currentThread().isInterrupted()) { -// try { -// RowChangeEvent event = nonTxQueue.poll(10, TimeUnit.MILLISECONDS); -// if (event != null) { -// dispatchImmediately(event, null); -// metricsFacade.recordTransaction(); -// continue; -// } -// -// activeTxContexts.values().forEach(ctx -> -// ctx.getTrackedTables().forEach(table -> -// checkPendingEvents(ctx, table) -// ) -// ); -// } catch (InterruptedException e) { -// Thread.currentThread().interrupt(); -// } -// } -// }); - } - - protected void dispatchImmediately(RowChangeEvent event, SinkContext ctx) { - dispatchExecutor.execute(() -> { - try { - LOGGER.debug("Dispatching [{}] {}.{} (Order: {}/{}) TX: {}", - event.getOp().name(), - event.getDb(), - event.getTable(), - event.getTransaction() != null ? - event.getTransaction().getDataCollectionOrder() : "N/A", - event.getTransaction() != null ? - event.getTransaction().getTotalOrder() : "N/A", - event.getTransaction().getId()); - Summary.Timer writeLatencyTimer = metricsFacade.startWriteLatencyTimer(); - boolean success = writer.write(event); - writeLatencyTimer.close(); - if (success) { - metricsFacade.recordTotalLatency(event); - metricsFacade.recordRowChange(event.getTable(), event.getOp()); - event.endLatencyTimer(); - } else { - // TODO retry? - } - - } finally { - if (ctx != null) { - ctx.updateCounter(event.getFullTableName()); - if (ctx.pendingEvents.decrementAndGet() == 0 && ctx.completed) { - ctx.completionFuture.complete(null); - } - } - } - }); - } - - private void startTimeoutChecker() { - timeoutScheduler.scheduleAtFixedRate(() -> { - activeTxContexts.entrySet().removeIf(entry -> { - SinkContext ctx = entry.getValue(); - if (ctx.isExpired()) { - LOGGER.warn("Transaction timeout: {}", entry.getKey()); - flushRemainingEvents(ctx); - return true; - } - return false; - }); - }, 10, 10, TimeUnit.SECONDS); - } - - private void flushRemainingEvents(SinkContext ctx) { - LOGGER.debug("Try Flush remaining events of {}", ctx.sourceTxId); - ctx.getTrackedTables().forEach(table -> { - String bufferKey = ctx.sourceTxId + "|" + table; - PriorityBlockingQueue buffer = orderedBuffers.remove(bufferKey); - if (buffer != null) { - LOGGER.warn("Flushing {} events for {}.{}", - buffer.size(), ctx.sourceTxId, table); - buffer.forEach(event -> { - LOGGER.debug("Processing event for {}:{}/{}", - ctx.sourceTxId, event.collectionOrder, event.totalOrder); - dispatchImmediately(event.event, ctx); - LOGGER.debug("End Event for {}:{}/{}", - ctx.sourceTxId, event.collectionOrder, event.totalOrder); - }); - } - }); - } - - private void handleNonTxEvent(RowChangeEvent event) { - // nonTxQueue.offer(event); - dispatchImmediately(event, null); - // event.endLatencyTimer(); - } - - public void shutdown() { - dispatchExecutor.shutdown(); - timeoutScheduler.shutdown(); - } - - public void setTxTimeoutMs(long txTimeoutMs) { - TX_TIMEOUT_MS = txTimeoutMs; - } - - private static class OrderedEvent { - final RowChangeEvent event; - final String table; - final long collectionOrder; - final long totalOrder; - - OrderedEvent(RowChangeEvent event, long collectionOrder, long totalOrder) { - this.event = event; - this.table = event.getFullTableName(); - this.collectionOrder = collectionOrder; - this.totalOrder = totalOrder; - } - - String getTable() { - return table; - } - - long getCollectionOrder() { - return collectionOrder; - } - } - - @Deprecated - private static class BufferedEvent { // useless - final RowChangeEvent event; - final long collectionOrder; - final long totalOrder; - - BufferedEvent(RowChangeEvent event, long collectionOrder, long totalOrder) { - this.event = event; - this.collectionOrder = collectionOrder; - this.totalOrder = totalOrder; - } - - long getTotalOrder() { - return totalOrder; - } - } - -} diff --git a/src/main/java/io/pixelsdb/pixels/sink/concurrent/TransactionCoordinatorFactory.java b/src/main/java/io/pixelsdb/pixels/sink/concurrent/TransactionCoordinatorFactory.java deleted file mode 100644 index d491fce..0000000 --- a/src/main/java/io/pixelsdb/pixels/sink/concurrent/TransactionCoordinatorFactory.java +++ /dev/null @@ -1,33 +0,0 @@ -/* - * 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.concurrent; - -public class TransactionCoordinatorFactory { - private static TransactionCoordinator instance; - - public static synchronized TransactionCoordinator getCoordinator() { - if (instance == null) { - instance = new TransactionCoordinator(); - } - return instance; - } - - public static synchronized void reset() { - instance = null; - } -} diff --git a/src/main/java/io/pixelsdb/pixels/sink/concurrent/TransactionManager.java b/src/main/java/io/pixelsdb/pixels/sink/concurrent/TransactionManager.java deleted file mode 100644 index 9a7b989..0000000 --- a/src/main/java/io/pixelsdb/pixels/sink/concurrent/TransactionManager.java +++ /dev/null @@ -1,74 +0,0 @@ -/* - * 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.concurrent; - -import io.pixelsdb.pixels.common.exception.TransException; -import io.pixelsdb.pixels.common.transaction.TransContext; -import io.pixelsdb.pixels.common.transaction.TransService; - -import java.util.List; -import java.util.Queue; -import java.util.concurrent.ConcurrentLinkedDeque; - -/** - * This class if for - * - * @author AntiO2 - */ -public class TransactionManager { - private final static TransactionManager instance = new TransactionManager(); - private final TransService transService; - private final Queue transContextQueue; - private final Object batchLock = new Object(); - - TransactionManager() { - this.transService = TransService.Instance(); - this.transContextQueue = new ConcurrentLinkedDeque<>(); - } - - public static TransactionManager Instance() { - return instance; - } - - private void requestTransactions() { - try { - List newContexts = transService.beginTransBatch(100, false); - transContextQueue.addAll(newContexts); - } catch (TransException e) { - throw new RuntimeException("Batch request failed", e); - } - } - - public TransContext getTransContext() { - TransContext ctx = transContextQueue.poll(); - if (ctx != null) { - return ctx; - } - synchronized (batchLock) { - ctx = transContextQueue.poll(); - if (ctx == null) { - requestTransactions(); - ctx = transContextQueue.poll(); - if (ctx == null) { - throw new IllegalStateException("No contexts available"); - } - } - return ctx; - } - } -} diff --git a/src/main/java/io/pixelsdb/pixels/sink/concurrent/TransactionState.java b/src/main/java/io/pixelsdb/pixels/sink/concurrent/TransactionState.java deleted file mode 100644 index 0897b3a..0000000 --- a/src/main/java/io/pixelsdb/pixels/sink/concurrent/TransactionState.java +++ /dev/null @@ -1,86 +0,0 @@ -/* - * 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.concurrent; - -import io.pixelsdb.pixels.sink.SinkProto; -import io.pixelsdb.pixels.sink.event.RowChangeEvent; - -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.CopyOnWriteArrayList; -import java.util.concurrent.atomic.AtomicInteger; -import java.util.stream.Collectors; -public class TransactionState { - private final String txId; - private final long beginTs; - private final Map receivedCounts = new ConcurrentHashMap<>(); - private final CopyOnWriteArrayList rowEvents = new CopyOnWriteArrayList<>(); - private Map expectedCounts; // update this when receive END message - private volatile boolean endReceived = false; - - public TransactionState(String txId) { - this.txId = txId; - this.beginTs = System.currentTimeMillis(); - this.expectedCounts = new HashMap<>(); - } - - public void addRowEvent(RowChangeEvent event) { - rowEvents.add(event); - String table = event.getTable(); - receivedCounts.compute(table, (k, v) -> { - if (v == null) { - return new AtomicInteger(1); - } else { - v.incrementAndGet(); - return v; - } - }); - } - - public boolean isComplete() { - return endReceived && - expectedCounts.entrySet().stream() - .allMatch(e -> receivedCounts.getOrDefault(e.getKey(), new AtomicInteger(0)).get() >= e.getValue()); - } - - public void markEndReceived() { - this.endReceived = true; - } - - public boolean isExpired(long timeoutMs) { - return System.currentTimeMillis() - beginTs > timeoutMs; - } - - public void setExpectedCounts(List dataCollectionList) { - this.expectedCounts = dataCollectionList.stream() - .collect(Collectors.toMap( - SinkProto.DataCollection::getDataCollection, - SinkProto.DataCollection::getEventCount - )); - } - - public void setExpectedCounts(Map dataCollectionMap) { - this.expectedCounts = dataCollectionMap; - } - - public List getRowEvents() { - return rowEvents; - } -} 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 86c177b..4afc3a8 100644 --- a/src/main/java/io/pixelsdb/pixels/sink/config/CommandLineConfig.java +++ b/src/main/java/io/pixelsdb/pixels/sink/config/CommandLineConfig.java @@ -1,24 +1,26 @@ /* * 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 + * This file is part of Pixels. * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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. * - * 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. + * 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.config; -import static net.sourceforge.argparse4j.impl.Arguments.storeTrue; import net.sourceforge.argparse4j.ArgumentParsers; import net.sourceforge.argparse4j.inf.ArgumentParser; import net.sourceforge.argparse4j.inf.ArgumentParserException; diff --git a/src/main/java/io/pixelsdb/pixels/sink/config/ConfigKey.java b/src/main/java/io/pixelsdb/pixels/sink/config/ConfigKey.java new file mode 100644 index 0000000..1a46ddf --- /dev/null +++ b/src/main/java/io/pixelsdb/pixels/sink/config/ConfigKey.java @@ -0,0 +1,43 @@ +/* + * 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.config; + + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * @package: io.pixelsdb.pixels.sink.config + * @className: ConfigKey + * @author: AntiO2 + * @date: 2025/9/26 13:04 + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.FIELD) +public @interface ConfigKey { + String value(); + + String defaultValue() default ""; + + Class defaultClass() default Void.class; +} \ No newline at end of file diff --git a/src/main/java/io/pixelsdb/pixels/sink/config/ConfigLoader.java b/src/main/java/io/pixelsdb/pixels/sink/config/ConfigLoader.java new file mode 100644 index 0000000..086638f --- /dev/null +++ b/src/main/java/io/pixelsdb/pixels/sink/config/ConfigLoader.java @@ -0,0 +1,93 @@ +/* + * 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.config; + + +import io.pixelsdb.pixels.sink.writer.PixelsSinkMode; +import io.pixelsdb.pixels.sink.writer.retina.RetinaServiceProxy; +import io.pixelsdb.pixels.sink.writer.retina.TransactionMode; + +import java.lang.reflect.Field; +import java.util.List; +import java.util.Properties; + +public class ConfigLoader { + public static void load(Properties props, Object target) { + try { + Class clazz = target.getClass(); + for (Field field : clazz.getDeclaredFields()) { + ConfigKey annotation = field.getAnnotation(ConfigKey.class); + if (annotation != null) { + String key = annotation.value(); + String value = props.getProperty(key); + if (value == null || value.isEmpty()) { + if (!annotation.defaultValue().isEmpty()) { + value = annotation.defaultValue(); + } else if (annotation.defaultClass() != Void.class) { + value = annotation.defaultClass().getName(); + } + } + Object parsed = convert(value, field.getType()); + field.setAccessible(true); + try { + field.set(target, parsed); + } catch (IllegalAccessException e) { + throw new RuntimeException("Failed to inject config for " + key, e); + } + } + } + } catch (Exception e) { + throw new RuntimeException("Failed to load config", e); + } + } + + private static Object convert(String value, Class type) { + if (type.equals(int.class) || type.equals(Integer.class)) { + return Integer.parseInt(value); + } else if (type.equals(long.class) || type.equals(Long.class)) { + return Long.parseLong(value); + } else if (type.equals(short.class) || type.equals(Short.class)) { + return Short.parseShort(value); + } else if (type.equals(boolean.class) || type.equals(Boolean.class)) { + return Boolean.parseBoolean(value); + } else if (type.equals(PixelsSinkMode.class)) { + return PixelsSinkMode.fromValue(value); + } else if (type.equals(TransactionMode.class)) { + return TransactionMode.fromValue(value); + } else if (type.equals(RetinaServiceProxy.RetinaWriteMode.class)) { + return RetinaServiceProxy.RetinaWriteMode.fromValue(value); + } else if (type.equals(List.class)) { + // Handle List type: split the string by comma (",") + // and return a List. Trimming each element is recommended. + if (value == null || value.isEmpty()) { + return java.util.Collections.emptyList(); + } + + // Use Arrays.asList(String.split(",")) to handle the splitting, + // then stream to trim whitespace from each element. + return java.util.Arrays.stream(value.split(",")) + .map(String::trim) + .collect(java.util.stream.Collectors.toList()); + } + return value; + } +} + 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 2b9e430..a1f77d2 100644 --- a/src/main/java/io/pixelsdb/pixels/sink/config/PixelsSinkConfig.java +++ b/src/main/java/io/pixelsdb/pixels/sink/config/PixelsSinkConfig.java @@ -1,147 +1,235 @@ /* * 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 + * This file is part of Pixels. * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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. * - * 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. + * 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.config; import io.pixelsdb.pixels.common.utils.ConfigFactory; -import io.pixelsdb.pixels.sink.sink.PixelsSinkMode; +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 java.io.IOException; -import java.util.Objects; +import java.util.List; @Getter public class PixelsSinkConfig { - private ConfigFactory config; + private final ConfigFactory config; + @ConfigKey(value = "transaction.timeout", defaultValue = TransactionConfig.DEFAULT_TRANSACTION_TIME_OUT) private Long transactionTimeout; + + @ConfigKey(value = "sink.mode", defaultValue = PixelsSinkDefaultConfig.SINK_MODE) private PixelsSinkMode pixelsSinkMode; + + @ConfigKey(value = "sink.retina.mode", defaultValue = PixelsSinkDefaultConfig.SINK_RETINA_MODE) + private RetinaServiceProxy.RetinaWriteMode retinaWriteMode; + + @ConfigKey(value = "sink.retina.client", defaultValue = "1") + private int retinaClientNum; + + @ConfigKey(value = "sink.retina.log.queue", defaultValue = "true") + private boolean retinaLogQueueEnabled; + + @ConfigKey(value = "sink.trans.mode", defaultValue = TransactionConfig.DEFAULT_TRANSACTION_MODE) + private TransactionMode transactionMode; + + @ConfigKey(value = "sink.commit.method", defaultValue = "async") + private String commitMethod; + + @ConfigKey(value = "sink.commit.batch.size", defaultValue = "500") + private int commitBatchSize; + + @ConfigKey(value = "sink.commit.batch.worker", defaultValue = "16") + private int commitBatchWorkers; + + @ConfigKey(value = "sink.commit.batch.delay", defaultValue = "200") + private int commitBatchDelay; + + @ConfigKey(value = "sink.remote.port", defaultValue = "9090") private short remotePort; - private int batchSize; + + @ConfigKey(value = "sink.flink.server.port", defaultValue = "9091") + private int sinkFlinkServerPort; + + @ConfigKey(value = "sink.timeout.ms", defaultValue = "30000") private int timeoutMs; + + @ConfigKey(value = "sink.flush.interval.ms", defaultValue = "1000") private int flushIntervalMs; + + @ConfigKey(value = "sink.flush.batch.size", defaultValue = "100") + private int flushBatchSize; + + @ConfigKey(value = "sink.max.retries", defaultValue = "3") private int maxRetries; + + @ConfigKey(value = "sink.csv.enable_header", defaultValue = "false") private boolean sinkCsvEnableHeader; + + @ConfigKey(value = "sink.monitor.enable", defaultValue = "false") private boolean monitorEnabled; + + @ConfigKey(value = "sink.monitor.port", defaultValue = "9464") private short monitorPort; + + @ConfigKey(value = "sink.monitor.report.enable", defaultValue = "true") + private boolean monitorReportEnabled; + + @ConfigKey(value = "sink.monitor.report.interval", defaultValue = "5000") + private short monitorReportInterval; + + @ConfigKey(value = "sink.monitor.freshness.interval", defaultValue = "1000") + private int freshnessReportInterval; + + @ConfigKey(value = "sink.monitor.freshness.file", defaultValue = "/tmp/sinkFreshness.csv") + private String monitorFreshnessReportFile; + + @ConfigKey(value = "sink.monitor.report.file", defaultValue = "/tmp/sink.csv") + private String monitorReportFile; + + @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; - public PixelsSinkConfig(String configFilePath) throws IOException { - this.config = ConfigFactory.Instance(); - this.config.loadProperties(configFilePath); - parseProps(); - } + private boolean retinaEmbedded = false; - public PixelsSinkConfig(ConfigFactory config) { - this.config = config; - parseProps(); - } + @ConfigKey("topic.prefix") + private String topicPrefix; + @ConfigKey("debezium.topic.prefix") + private String debeziumTopicPrefix; - private void parseProps() { - this.pixelsSinkMode = PixelsSinkMode.fromValue(getProperty("sink.mode", PixelsSinkDefaultConfig.SINK_MODE)); - this.transactionTimeout = Long.valueOf(getProperty("transaction.timeout", TransactionConfig.DEFAULT_TRANSACTION_TIME_OUT)); - this.remotePort = parseShort(getProperty("sink.remote.port"), PixelsSinkDefaultConfig.SINK_REMOTE_PORT); - this.batchSize = parseInt(getProperty("sink.batch.size"), PixelsSinkDefaultConfig.SINK_BATCH_SIZE); - this.timeoutMs = parseInt(getProperty("sink.timeout.ms"), PixelsSinkDefaultConfig.SINK_TIMEOUT_MS); - this.flushIntervalMs = parseInt(getProperty("sink.flush.interval.ms"), PixelsSinkDefaultConfig.SINK_FLUSH_INTERVAL_MS); - this.maxRetries = parseInt(getProperty("sink.max.retries"), PixelsSinkDefaultConfig.SINK_MAX_RETRIES); - this.sinkCsvEnableHeader = parseBoolean(getProperty("sink.csv.enable_header"), PixelsSinkDefaultConfig.SINK_CSV_ENABLE_HEADER); - this.monitorEnabled = parseBoolean(getProperty("sink.monitor.enabled"), PixelsSinkDefaultConfig.SINK_MONITOR_ENABLED); - this.monitorPort = parseShort(getProperty("sink.monitor.port"), PixelsSinkDefaultConfig.SINK_MONITOR_PORT); - this.rpcEnable = parseBoolean(getProperty("sink.rpc.enable"), PixelsSinkDefaultConfig.SINK_RPC_ENABLED); - this.mockRpcDelay = parseInt(getProperty("sink.rpc.mock.delay"), PixelsSinkDefaultConfig.MOCK_RPC_DELAY); - this.transBatchSize = parseInt(getProperty("sink.trans.batch.size"), PixelsSinkDefaultConfig.TRANSACTION_BATCH_SIZE); - } + @ConfigKey("consumer.capture_database") + private String captureDatabase; - public String getTopicPrefix() { - return getProperty("topic.prefix"); - } + @ConfigKey(value = "consumer.include_tables", defaultValue = "") + private String includeTablesRaw; - public String getCaptureDatabase() { - return getProperty("consumer.capture_database"); - } + @ConfigKey("bootstrap.servers") + private String bootstrapServers; - public String[] getIncludeTables() { - String includeTables = getProperty("consumer.include_tables", ""); - return includeTables.isEmpty() ? new String[0] : includeTables.split(","); - } + @ConfigKey("group.id") + private String groupId; - public String getBootstrapServers() { - return getProperty("bootstrap.servers"); - } + @ConfigKey(value = "key.deserializer", defaultClass = StringDeserializer.class) + private String keyDeserializer; - public String getGroupId() { - return getProperty("group.id"); - } + @ConfigKey(value = "value.deserializer", defaultClass = RowChangeEventJsonDeserializer.class) + private String valueDeserializer; - public String getKeyDeserializer() { - return getProperty("key.deserializer", PixelsSinkDefaultConfig.KEY_DESERIALIZER); - } - public String getValueDeserializer() { - return getProperty("value.deserializer", PixelsSinkDefaultConfig.VALUE_DESERIALIZER); - } + @ConfigKey(value = "sink.csv.path", defaultValue = PixelsSinkDefaultConfig.CSV_SINK_PATH) + private String csvSinkPath; - public String getCsvSinkPath() { - return getProperty("sink.csv.path", PixelsSinkDefaultConfig.CSV_SINK_PATH); - } + @ConfigKey(value = "transaction.topic.suffix", defaultValue = TransactionConfig.DEFAULT_TRANSACTION_TOPIC_SUFFIX) + private String transactionTopicSuffix; - public String getTransactionTopicSuffix() { - return getProperty("transaction.topic.suffix", TransactionConfig.DEFAULT_TRANSACTION_TOPIC_SUFFIX); - } + @ConfigKey(value = "transaction.topic.value.deserializer", + defaultClass = RowChangeEventJsonDeserializer.class) + private String transactionTopicValueDeserializer; - public String getTransactionTopicValueDeserializer() { - return getProperty("transaction.topic.value.deserializer", TransactionConfig.DEFAULT_TRANSACTION_TOPIC_VALUE_DESERIALIZER); - } + @ConfigKey(value = "transaction.topic.group_id", + defaultValue = TransactionConfig.DEFAULT_TRANSACTION_TOPIC_GROUP_ID) + private String transactionTopicGroupId; - public String getTransactionTopicGroupId() { - return getProperty("transaction.topic.group_id", TransactionConfig.DEFAULT_TRANSACTION_TOPIC_GROUP_ID); - } + @ConfigKey(value = "sink.remote.host", defaultValue = PixelsSinkDefaultConfig.SINK_REMOTE_HOST) + private String sinkRemoteHost; - public String getSinkRemoteHost() { - return getProperty("sink.remote.host", PixelsSinkDefaultConfig.SINK_REMOTE_HOST); - } + @ConfigKey("sink.registry.url") + private String registryUrl; - private short parseShort(String valueStr, short defaultValue) { - return (valueStr != null) ? Short.parseShort(valueStr) : defaultValue; - } + @ConfigKey(value = "sink.datasource", defaultValue = PixelsSinkDefaultConfig.DATA_SOURCE) + private String dataSource; - private int parseInt(String valueStr, int defaultValue) { - return (valueStr != null) ? Integer.parseInt(valueStr) : defaultValue; - } + @ConfigKey(value = "sink.datasource.rate.limit", defaultValue = "-1") + private int sourceRateLimit; - private boolean parseBoolean(String valueStr, boolean defaultValue) { - return (valueStr != null) ? Boolean.parseBoolean(valueStr) : defaultValue; - } + private boolean enableSourceRateLimit; + + @ConfigKey(value = "sink.proto.dir") + private String sinkProtoDir; + @ConfigKey(value = "sink.proto.data", defaultValue = "data") + private String sinkProtoData; + + @ConfigKey(value = "sink.proto.maxRecords", defaultValue = PixelsSinkDefaultConfig.MAX_RECORDS_PER_FILE) + private int maxRecordsPerFile; + + @ConfigKey(value = "sink.storage.loop", defaultValue = "false") + private boolean sinkStorageLoop; - public String getProperty(String key) { - return config.getProperty(key); + @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") + private Integer sinkMonitorFreshnessEmbedWarmupSeconds; + + @ConfigKey(value = "sink.monitor.freshness.embed.static", defaultValue = "false") + private boolean sinkMonitorFreshnessEmbedStatic; + + @ConfigKey(value = "sink.monitor.freshness.embed.snapshot", defaultValue = "false") + private boolean sinkMonitorFreshnessEmbedSnapshot; + + @ConfigKey(value = "sink.monitor.freshness.embed.tablelist", defaultValue = "false") + private List sinkMonitorFreshnessEmbedTableList; + + @ConfigKey(value = "sink.monitor.freshness.verbose", defaultValue = "false") + private boolean sinkMonitorFreshnessVerbose; + + @ConfigKey(value = "sink.monitor.freshness.timestamp", defaultValue = "false") + private boolean sinkMonitorFreshnessTimestamp; + + @ConfigKey(value = "trino.url") + private String trinoUrl; + + @ConfigKey(value = "trino.user") + private String trinoUser; + + @ConfigKey(value = "trino.password") + private String trinoPassword; + + @ConfigKey(value = "trino.parallel", defaultValue = "1") + private int trinoParallel; + + public PixelsSinkConfig(String configFilePath) throws IOException { + this.config = ConfigFactory.Instance(); + this.config.loadProperties(configFilePath); + init(); } - public String getProperty(String key, String defaultValue) { - String value = config.getProperty(key); - if (Objects.isNull(value)) { - return defaultValue; - } - return value; + public PixelsSinkConfig(ConfigFactory config) { + this.config = config; + init(); } - public String getRegistryUrl() { - return getProperty("sink.registry.url", ""); + public String[] getIncludeTables() { + return includeTablesRaw.isEmpty() ? new String[0] : includeTablesRaw.split(","); } + private void init() { + ConfigLoader.load(this.config.extractPropertiesByPrefix("", false), this); + + this.enableSourceRateLimit = this.sourceRateLimit >= 0; + } } \ 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 9a3c354..3e16fa3 100644 --- a/src/main/java/io/pixelsdb/pixels/sink/config/PixelsSinkConstants.java +++ b/src/main/java/io/pixelsdb/pixels/sink/config/PixelsSinkConstants.java @@ -1,18 +1,21 @@ /* * 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 + * This file is part of Pixels. * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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. * - * 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. + * 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.config; @@ -21,8 +24,9 @@ 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 int MAX_QUEUE_SIZE = 1_000; public static final String SNAPSHOT_TX_PREFIX = "SNAPSHOT-"; - private PixelsSinkConstants() {} + private PixelsSinkConstants() { + } } 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 ae64db3..2bd04bc 100644 --- a/src/main/java/io/pixelsdb/pixels/sink/config/PixelsSinkDefaultConfig.java +++ b/src/main/java/io/pixelsdb/pixels/sink/config/PixelsSinkDefaultConfig.java @@ -1,49 +1,40 @@ /* * 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 + * This file is part of Pixels. * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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. * - * 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. + * 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.config; -import io.pixelsdb.pixels.sink.deserializer.RowChangeEventJsonDeserializer; -import org.apache.kafka.common.serialization.StringDeserializer; - public class PixelsSinkDefaultConfig { + public static final String DATA_SOURCE = "kafka"; public static final String PROPERTIES_PATH = "pixels-sink.properties"; public static final String CSV_SINK_PATH = "./data"; - public static final String KEY_DESERIALIZER = StringDeserializer.class.getName(); // org.apache.kafka.common.serialization.StringDeserializer - public static final String VALUE_DESERIALIZER = RowChangeEventJsonDeserializer.class.getName(); - public static final String SINK_MODE = "csv"; public static final int SINK_CSV_RECORD_FLUSH = 1000; public static final int SINK_THREAD = 32; public static final int SINK_CONSUMER_THREAD = 8; - // sink.remote.host=localhost -// sink.remote.port=229422 -// sink.batch.size=100 -// sink.timeout.ms=5000 -// sink.flush.interval.ms=5000 -// sink.max.retries=3 // Transaction Service public static final int TRANSACTION_BATCH_SIZE = 100; - // REMOTE BUFFER public static final String SINK_REMOTE_HOST = "localhost"; public static final short SINK_REMOTE_PORT = 22942; @@ -52,6 +43,7 @@ public class PixelsSinkDefaultConfig { public static final int SINK_FLUSH_INTERVAL_MS = 5000; public static final int SINK_MAX_RETRIES = 3; public static final boolean SINK_CSV_ENABLE_HEADER = false; + public static final String SINK_RETINA_MODE = "stub"; // Monitor Config public static final boolean SINK_MONITOR_ENABLED = true; @@ -60,4 +52,5 @@ 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"; } 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 b762631..cab407a 100644 --- a/src/main/java/io/pixelsdb/pixels/sink/config/TransactionConfig.java +++ b/src/main/java/io/pixelsdb/pixels/sink/config/TransactionConfig.java @@ -1,30 +1,34 @@ /* * 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 + * This file is part of Pixels. * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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. * - * 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. + * 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.config; -import io.pixelsdb.pixels.sink.deserializer.TransactionJsonMessageDeserializer; +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_TOPIC_GROUP_ID = "transaction_consumer"; public static final String DEFAULT_TRANSACTION_TIME_OUT = "300"; public static final String DEFAULT_TRANSACTION_BATCH_SIZE = "100"; + public static final String DEFAULT_TRANSACTION_MODE = "batch"; } 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 04492a4..405a245 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,18 +1,21 @@ /* * 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 + * This file is part of Pixels. * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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. * - * 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. + * 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.config.factory; 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 d243050..0ec0736 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,18 +1,21 @@ /* * 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 + * This file is part of Pixels. * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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. * - * 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. + * 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.config.factory; 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 77a320b..413bc0d 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,18 +1,21 @@ /* * 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 + * This file is part of Pixels. * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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. * - * 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. + * 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.config.factory; @@ -26,6 +29,7 @@ public class PixelsSinkConfigFactory { private static volatile PixelsSinkConfig instance; private static String configFilePath; private static ConfigFactory config; + private PixelsSinkConfigFactory() { } 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 d1d4336..a31e376 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,18 +1,21 @@ /* * 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 + * This file is part of Pixels. * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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. * - * 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. + * 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.config.factory; @@ -22,20 +25,26 @@ import java.util.Properties; -public class RowRecordKafkaPropFactory implements KafkaPropFactory{ +public class RowRecordKafkaPropFactory implements KafkaPropFactory { + static Properties getCommonKafkaProperties(PixelsSinkConfig config) { + Properties kafkaProperties = new Properties(); + 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"); + return kafkaProperties; + } + @Override public Properties createKafkaProperties(PixelsSinkConfig config) { Properties kafkaProperties = getCommonKafkaProperties(config); kafkaProperties.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, config.getValueDeserializer()); kafkaProperties.put(ConsumerConfig.GROUP_ID_CONFIG, config.getGroupId()); - return kafkaProperties; - } - static Properties getCommonKafkaProperties(PixelsSinkConfig config) { - Properties kafkaProperties = new Properties(); - 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(ConsumerConfig.SESSION_TIMEOUT_MS_CONFIG, "30000"); + kafkaProperties.put(ConsumerConfig.HEARTBEAT_INTERVAL_MS_CONFIG, "1000"); + kafkaProperties.put(ConsumerConfig.MAX_POLL_INTERVAL_MS_CONFIG, "600000"); + kafkaProperties.put(ConsumerConfig.MAX_POLL_RECORDS_CONFIG, "500"); + return kafkaProperties; } } 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 4221c2c..e97b731 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,18 +1,21 @@ /* * 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 + * This file is part of Pixels. * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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. * - * 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. + * 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.config.factory; @@ -24,10 +27,10 @@ import static io.pixelsdb.pixels.sink.config.factory.RowRecordKafkaPropFactory.getCommonKafkaProperties; -public class TransactionKafkaPropFactory implements KafkaPropFactory{ +public class TransactionKafkaPropFactory implements KafkaPropFactory { @Override public Properties createKafkaProperties(PixelsSinkConfig config) { - Properties kafkaProperties = getCommonKafkaProperties(config); + Properties kafkaProperties = getCommonKafkaProperties(config); kafkaProperties.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, config.getTransactionTopicValueDeserializer()); kafkaProperties.put(ConsumerConfig.GROUP_ID_CONFIG, config.getTransactionTopicGroupId() + "-" + config.getGroupId()); return kafkaProperties; diff --git a/src/main/java/io/pixelsdb/pixels/sink/deserializer/DeserializerUtil.java b/src/main/java/io/pixelsdb/pixels/sink/deserializer/DeserializerUtil.java deleted file mode 100644 index 2c39ec8..0000000 --- a/src/main/java/io/pixelsdb/pixels/sink/deserializer/DeserializerUtil.java +++ /dev/null @@ -1,99 +0,0 @@ -/* - * 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.deserializer; - -import com.google.protobuf.ByteString; -import io.pixelsdb.pixels.sink.SinkProto; -import io.pixelsdb.pixels.sink.event.RowChangeEvent; -import org.apache.avro.AvroRuntimeException; -import org.apache.avro.generic.GenericRecord; - -import java.util.Arrays; - -public class DeserializerUtil { - static RowChangeEvent buildErrorEvent(String topic, byte[] rawData, Exception error) { - SinkProto.ErrorInfo errorInfo = SinkProto.ErrorInfo.newBuilder() - .setMessage(error.getMessage()) - .setStackTrace(Arrays.toString(error.getStackTrace())) - .setOriginalData(ByteString.copyFrom(rawData)) - .build(); - - SinkProto.RowRecord record = SinkProto.RowRecord.newBuilder() - .setOp(SinkProto.OperationType.UNRECOGNIZED) - .setTsMs(System.currentTimeMillis()) - .build(); - - return new RowChangeEvent(record) { - @Override - public boolean hasError() { - return true; - } - - @Override - public SinkProto.ErrorInfo getErrorInfo() { - return errorInfo; - } - - @Override - public String getTopic() { - return topic; - } - }; - } - - static public SinkProto.TransactionStatus getStatusSafely(GenericRecord record, String field) { - String statusString = getStringSafely(record, field); - if (statusString.equals("BEGIN")) { - return SinkProto.TransactionStatus.BEGIN; - } - if (statusString.equals("END")) { - return SinkProto.TransactionStatus.END; - } - - return SinkProto.TransactionStatus.UNRECOGNIZED; - } - - static public String getStringSafely(GenericRecord record, String field) { - try { - Object value = record.get(field); - return value != null ? value.toString() : ""; - } catch (AvroRuntimeException e) { - return ""; - } - } - - static public Long getLongSafely(GenericRecord record, String field) { - try { - Object value = record.get(field); - return value instanceof Number ? ((Number) value).longValue() : 0L; - } catch (AvroRuntimeException e) { - return 0L; - } - } - - static public SinkProto.OperationType getOperationType(String op) { - op = op.toLowerCase(); - return switch (op) { - 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)); - }; - } -} diff --git a/src/main/java/io/pixelsdb/pixels/sink/deserializer/RowChangeEventJsonDeserializer.java b/src/main/java/io/pixelsdb/pixels/sink/deserializer/RowChangeEventJsonDeserializer.java deleted file mode 100644 index bf5f002..0000000 --- a/src/main/java/io/pixelsdb/pixels/sink/deserializer/RowChangeEventJsonDeserializer.java +++ /dev/null @@ -1,161 +0,0 @@ -/* - * 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.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.monitor.MetricsFacade; -import org.apache.kafka.common.serialization.Deserializer; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.util.Map; - -public class RowChangeEventJsonDeserializer implements Deserializer { - private static final Logger logger = LoggerFactory.getLogger(RowChangeEventJsonDeserializer.class); - private static final ObjectMapper objectMapper = new ObjectMapper(); - - @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 schemaNode = rootNode.path("schema"); - JsonNode payloadNode = rootNode.path("payload"); - - SinkProto.OperationType opType = parseOperationType(payloadNode); - TypeDescription schema = getSchema(schemaNode, opType); - - return buildRowRecord(payloadNode, schema, opType); - } catch (Exception e) { - logger.error("Failed to deserialize message from topic {}: {}", topic, e.getMessage()); - return DeserializerUtil.buildErrorEvent(topic, data, e); - } - } - - private SinkProto.OperationType parseOperationType(JsonNode payloadNode) { - String opCode = payloadNode.path("op").asText(""); - return DeserializerUtil.getOperationType(opCode); - } - - // TODO: cache schema - private TypeDescription getSchema(JsonNode schemaNode, SinkProto.OperationType opType) { - switch (opType) { - case DELETE: - return SchemaDeserializer.parseFromBeforeOrAfter(schemaNode, "before"); - case INSERT: - case UPDATE: - case SNAPSHOT: - return SchemaDeserializer.parseFromBeforeOrAfter(schemaNode, "after"); - case UNRECOGNIZED: - throw new IllegalArgumentException("Operation type is unknown. Check op"); - } - return null; - } - - private RowChangeEvent buildRowRecord(JsonNode payloadNode, - TypeDescription schema, - SinkProto.OperationType opType) { - - 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()); - - Map beforeData = parseDataFields(payloadNode, schema, opType, "before"); - Map afterData = parseDataFields(payloadNode, schema, opType, "after"); - if (payloadNode.has("source")) { - builder.setSource(parseSourceInfo(payloadNode.get("source"))); - } - - if (payloadNode.hasNonNull("transaction")) { - builder.setTransaction(parseTransactionInfo(payloadNode.get("transaction"))); - } - - // RowChangeEvent event = new RowChangeEvent(builder.build(), schema, opType, beforeData, afterData); - RowChangeEvent event = new RowChangeEvent(builder.build()); - event.initIndexKey(); - return event; - } - - private Map parseDataFields(JsonNode payloadNode, - TypeDescription schema, - SinkProto.OperationType opType, - String dataField) { - RowDataParser parser = new RowDataParser(schema); - - JsonNode dataNode = payloadNode.get(dataField); - if (dataNode != null && !dataNode.isNull()) { - return parser.parse(dataNode, opType); - } - return null; - } - - private JsonNode resolveDataNode(JsonNode payloadNode, SinkProto.OperationType opType) { - return opType == SinkProto.OperationType.DELETE ? - payloadNode.get("before") : - payloadNode.get("after"); - } - - - private SinkProto.SourceInfo parseSourceInfo(JsonNode sourceNode) { - return SinkProto.SourceInfo.newBuilder() - .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()) - .setDb(sourceNode.path("db").asText()) - .setSequence(sourceNode.path("sequence").asText()) - .setTsUs(sourceNode.path("ts_us").asLong()) - .setTsNs(sourceNode.path("ts_ns").asLong()) - .setSchema(sourceNode.path("schema").asText()) - .setTable(sourceNode.path("table").asText()) - .setTxId(sourceNode.path("txId").asLong()) - .setLsn(sourceNode.path("lsn").asLong()) - .setXmin(sourceNode.path("xmin").asLong()) - .build(); - } - - private SinkProto.TransactionInfo parseTransactionInfo(JsonNode txNode) { - return SinkProto.TransactionInfo.newBuilder() - .setId(txNode.path("id").asText()) - .setTotalOrder(txNode.path("total_order").asLong()) - .setDataCollectionOrder(txNode.path("data_collection_order").asLong()) - .build(); - } - - - private boolean hasAfterData(SinkProto.OperationType op) { - return op != SinkProto.OperationType.DELETE; - } - - private boolean hasBeforeData(SinkProto.OperationType op) { - return op == SinkProto.OperationType.DELETE || op == SinkProto.OperationType.UPDATE; - } -} - diff --git a/src/main/java/io/pixelsdb/pixels/sink/deserializer/RowDataParser.java b/src/main/java/io/pixelsdb/pixels/sink/deserializer/RowDataParser.java deleted file mode 100644 index 43f9b26..0000000 --- a/src/main/java/io/pixelsdb/pixels/sink/deserializer/RowDataParser.java +++ /dev/null @@ -1,160 +0,0 @@ -/* - * 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.deserializer; - -import com.fasterxml.jackson.databind.JsonNode; -import io.pixelsdb.pixels.core.PixelsProto; -import io.pixelsdb.pixels.core.TypeDescription; -import io.pixelsdb.pixels.retina.RetinaProto; -import io.pixelsdb.pixels.sink.SinkProto; -import org.apache.avro.generic.GenericRecord; - -import java.io.IOException; -import java.math.BigDecimal; -import java.math.BigInteger; -import java.nio.ByteBuffer; -import java.time.LocalDate; -import java.util.Base64; -import java.util.Collections; -import java.util.HashMap; -import java.util.Map; -class RowDataParser { - private final TypeDescription schema; - - public RowDataParser(TypeDescription schema) { - this.schema = schema; - } - - public Map parse(JsonNode dataNode, SinkProto.OperationType operation) { - if (dataNode.isNull() && operation == SinkProto.OperationType.DELETE) { - return parseDeleteRecord(); - } - return parseNode(dataNode, schema); - } - - public void parse(GenericRecord record, SinkProto.RowValue.Builder builder) { - for (int i = 0; i < schema.getFieldNames().size(); i++) { - String fieldName = schema.getFieldNames().get(i); - TypeDescription fieldType = schema.getChildren().get(i); - builder.addValues(parseValue(record, fieldName, fieldType).build()); - // result.put(fieldName, parseValue(node.get(fieldName), fieldType)); - } - } - - private Map parseNode(JsonNode node, TypeDescription schema) { - Map result = new HashMap<>(); - for (int i = 0; i < schema.getFieldNames().size(); i++) { - String fieldName = schema.getFieldNames().get(i); - TypeDescription fieldType = schema.getChildren().get(i); - result.put(fieldName, parseValue(node.get(fieldName), fieldType)); - } - return result; - } - - private Object parseValue(JsonNode valueNode, TypeDescription type) { - if (valueNode.isNull()) return null; - - switch (type.getCategory()) { - case INT: - return valueNode.asInt(); - case LONG: - return valueNode.asLong(); - case STRING: - return valueNode.asText().trim(); - case DECIMAL: - return parseDecimal(valueNode, type); - case DATE: - return parseDate(valueNode); - case STRUCT: - return parseNode(valueNode, type); - case BINARY: - return parseBinary(valueNode); - default: - throw new IllegalArgumentException("Unsupported type: " + type); - } - } - - private SinkProto.ColumnValue.Builder parseValue(GenericRecord record, String filedName, TypeDescription filedType) { - - SinkProto.ColumnValue.Builder columnValueBuilder = SinkProto.ColumnValue.newBuilder(); - columnValueBuilder.setName(filedName); - switch (filedType.getCategory()) { - case INT: { - int value = (int) record.get(filedName); - columnValueBuilder.setValue(RetinaProto.ColumnValue.newBuilder().setNumberVal(Integer.toString(value))); - columnValueBuilder.setType(PixelsProto.Type.newBuilder().setKind(PixelsProto.Type.Kind.INT)); - break; - } - - case LONG: { - long value = (long) record.get(filedName); - columnValueBuilder.setValue(RetinaProto.ColumnValue.newBuilder().setNumberVal(Long.toString(value))); - columnValueBuilder.setType(PixelsProto.Type.newBuilder().setKind(PixelsProto.Type.Kind.LONG)); - break; - } - - case STRING: { - String value = (String) record.get(filedName).toString(); - columnValueBuilder.setValue(RetinaProto.ColumnValue.newBuilder().setStringVal(value)); - columnValueBuilder.setType(PixelsProto.Type.newBuilder().setKind(PixelsProto.Type.Kind.STRING)); - break; - } - case DECIMAL: { - columnValueBuilder.setType(PixelsProto.Type.newBuilder() - .setKind(PixelsProto.Type.Kind.DECIMAL) - .setDimension(filedType.getDimension()) - .setScale(filedType.getScale()) - .build()); - columnValueBuilder.setValue(RetinaProto.ColumnValue.newBuilder().setNumberVal( - new String(((ByteBuffer) record.get(filedName)).array()))); - break; - } -// case DATE: -// return parseDate(valueNode); -// case STRUCT: -// return parseNode(valueNode, type); -// case BINARY: -// return parseBinary(valueNode); - default: - throw new IllegalArgumentException("Unsupported type: " + filedType.getCategory()); - } - return columnValueBuilder; - } - - private Map parseDeleteRecord() { - return Collections.singletonMap("__deleted", true); - } - - BigDecimal parseDecimal(JsonNode node, TypeDescription type) { - byte[] bytes = Base64.getDecoder().decode(node.asText()); - int scale = type.getScale(); - 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/deserializer/RowRecordDeserializer.java b/src/main/java/io/pixelsdb/pixels/sink/deserializer/RowRecordDeserializer.java deleted file mode 100644 index 1a9c266..0000000 --- a/src/main/java/io/pixelsdb/pixels/sink/deserializer/RowRecordDeserializer.java +++ /dev/null @@ -1,62 +0,0 @@ -/* - * 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.deserializer; - -import com.fasterxml.jackson.databind.ObjectMapper; -import com.google.protobuf.InvalidProtocolBufferException; -import com.google.protobuf.util.JsonFormat; -import io.pixelsdb.pixels.sink.SinkProto; -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 RowRecordDeserializer implements Deserializer { - - private static final Logger LOGGER = LoggerFactory.getLogger(RowRecordDeserializer.class); - private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); - private static final JsonFormat.Parser PROTO_PARSER = JsonFormat.parser().ignoringUnknownFields(); - - static SinkProto.RowRecord parseRowRecord(Map rawMessage) throws IOException { - SinkProto.RowRecord.Builder builder = SinkProto.RowRecord.newBuilder(); - String json = OBJECT_MAPPER.writeValueAsString(rawMessage.get("payload")); - //TODO optimize - return getRowRecord(json, builder); - } - - private static SinkProto.RowRecord getRowRecord(String json, SinkProto.RowRecord.Builder builder) throws InvalidProtocolBufferException { - PROTO_PARSER.merge(json, builder); - return builder.build(); - } - - @Override - public SinkProto.RowRecord deserialize(String topic, byte[] data) { - if (data == null || data.length == 0) { - return null; - } - try { - Map rawMessage = OBJECT_MAPPER.readValue(data, Map.class); - return parseRowRecord(rawMessage); - } catch (IOException e) { - LOGGER.error("Failed to deserialize row record message", e); - throw new RuntimeException("Deserialization error", e); - } - } -} \ No newline at end of file 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 8a592c7..101dbef 100644 --- a/src/main/java/io/pixelsdb/pixels/sink/event/RowChangeEvent.java +++ b/src/main/java/io/pixelsdb/pixels/sink/event/RowChangeEvent.java @@ -1,149 +1,216 @@ /* * 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 + * This file is part of Pixels. * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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. * - * 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. + * 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; import com.google.protobuf.ByteString; -import io.pixelsdb.pixels.common.metadata.domain.SecondaryIndex; +import io.pixelsdb.pixels.common.metadata.SchemaTableName; +import io.pixelsdb.pixels.common.metadata.domain.SinglePointIndex; +import io.pixelsdb.pixels.common.utils.RetinaUtils; import io.pixelsdb.pixels.core.TypeDescription; import io.pixelsdb.pixels.index.IndexProto; -import io.pixelsdb.pixels.retina.RetinaProto; 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; -import io.pixelsdb.pixels.sink.monitor.MetricsFacade; +import io.pixelsdb.pixels.sink.util.MetricsFacade; import io.prometheus.client.Summary; import lombok.Getter; import lombok.Setter; import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.stream.IntStream; public class RowChangeEvent { @Getter private final SinkProto.RowRecord rowRecord; - private IndexProto.IndexKey indexKey; - private boolean isIndexKeyInited; - @Setter - private SecondaryIndex indexInfo; + private final MetricsFacade metricsFacade = MetricsFacade.getInstance(); + @Getter + private final TypeDescription schema; /** * timestamp from pixels transaction server */ @Setter @Getter private long timeStamp; - - @Getter - private final TypeDescription schema; - @Getter private String topic; - @Getter private TableMetadata tableMetadata = null; - - private final MetricsFacade metricsFacade = MetricsFacade.getInstance(); private Summary.Timer latencyTimer; private Map beforeValueMap; private Map afterValueMap; + @Getter + private IndexProto.IndexKey beforeKey; + @Getter + private IndexProto.IndexKey afterKey; + + private boolean indexKeyInited = false; + + @Getter + private long tableId; - public RowChangeEvent(SinkProto.RowRecord rowRecord) { + @Getter + private SchemaTableName schemaTableName; + + public RowChangeEvent(SinkProto.RowRecord rowRecord) throws SinkException { this.rowRecord = rowRecord; - this.schema = null; + TableMetadataRegistry tableMetadataRegistry = TableMetadataRegistry.Instance(); + this.schema = tableMetadataRegistry.getTypeDescription(getSchemaName(), getTable()); + init(); + initIndexKey(); } - - public RowChangeEvent(SinkProto.RowRecord rowRecord, TypeDescription schema) { + public RowChangeEvent(SinkProto.RowRecord rowRecord, TypeDescription schema) throws SinkException { this.rowRecord = rowRecord; this.schema = schema; + + init(); + // initIndexKey(); + } + + protected static int getBucketFromIndexKey(IndexProto.IndexKey indexKey) { + return getBucketIdFromByteBuffer(indexKey.getKey()); + } + + protected static int getBucketIdFromByteBuffer(ByteString byteString) { + return RetinaUtils.getBucketIdFromByteBuffer(byteString); + } + + private void init() throws SinkException { + TableMetadataRegistry tableMetadataRegistry = TableMetadataRegistry.Instance(); + this.tableId = tableMetadataRegistry.getTableId(getSchemaName(), getTable()); + this.schemaTableName = new SchemaTableName(getSchemaName(), getTable()); + + initColumnValueMap(); } private void initColumnValueMap() { if (hasBeforeData()) { + this.beforeValueMap = new HashMap<>(); initColumnValueMap(rowRecord.getBefore(), beforeValueMap); } if (hasAfterData()) { + this.afterValueMap = new HashMap<>(); initColumnValueMap(rowRecord.getAfter(), afterValueMap); } } private void initColumnValueMap(SinkProto.RowValue rowValue, Map map) { - rowValue.getValuesList().forEach( - column -> { - map.put(column.getName(), column); - } - ); + IntStream.range(0, schema.getFieldNames().size()) + .forEach(i -> map.put(schema.getFieldNames().get(i), rowValue.getValuesList().get(i))); } - public void setTimeStamp(long timeStamp) { - this.timeStamp = timeStamp; + public void initIndexKey() throws SinkException { + if (indexKeyInited) { + return; + } + + this.tableMetadata = TableMetadataRegistry.Instance().getMetadata( + this.rowRecord.getSource().getDb(), + this.rowRecord.getSource().getTable()); + + if (!this.tableMetadata.hasPrimaryIndex()) { + return; + } + if (hasBeforeData()) { + this.beforeKey = generateIndexKey(tableMetadata, beforeValueMap); + } + + if (hasAfterData()) { + this.afterKey = generateIndexKey(tableMetadata, afterValueMap); + } + + indexKeyInited = true; } - public void setIndexInfo(SecondaryIndex indexInfo) { - this.indexInfo = indexInfo; + public void updateIndexKey() throws SinkException { + if (hasBeforeData()) { + this.beforeKey = generateIndexKey(tableMetadata, beforeValueMap); + } + + if (hasAfterData()) { + this.afterKey = generateIndexKey(tableMetadata, afterValueMap); + } } - public IndexProto.IndexKey getIndexKey() { - if (!isIndexKeyInited) { - initIndexKey(); + public int getBeforeBucketFromIndex() { + assert indexKeyInited; + if (hasBeforeData()) { + return getBucketFromIndexKey(beforeKey); } - return indexKey; + throw new IllegalCallerException("Event dosen't have before data"); } - public void initIndexKey() { - if (!hasAfterData()) { - // We do not need to generate an index key for insert request - return; + public boolean isPkChanged() throws SinkException { + if (!indexKeyInited) { + initIndexKey(); } - this.tableMetadata = TableMetadataRegistry.Instance().getMetadata( - this.rowRecord.getSource().getDb(), - this.rowRecord.getSource().getTable()); - List keyColumnNames = tableMetadata.getKeyColumnNames(); - ByteBuffer byteBuffer = ByteBuffer.allocate(1024); + if (getOp() != SinkProto.OperationType.UPDATE) { + return false; + } + ByteString beforeKey = getBeforeKey().getKey(); + ByteString afterKey = getAfterKey().getKey(); - for (int i = 0; i < keyColumnNames.size(); i++) { - String name = keyColumnNames.get(i); - byteBuffer.put(afterValueMap.get(name).getValue().toByteArray()); - if (i < keyColumnNames.size() - 1) { - byteBuffer.putChar(':'); - } - } + return !beforeKey.equals(afterKey); + } - this.indexKey = IndexProto.IndexKey.newBuilder() - .setTimestamp(timeStamp) - .setKey(ByteString.copyFrom(byteBuffer)) - .setIndexId(indexInfo.getId()) - .build(); - isIndexKeyInited = true; + public int getAfterBucketFromIndex() { + assert indexKeyInited; + if (hasAfterData()) { + return getBucketFromIndexKey(afterKey); + } + throw new IllegalCallerException("Event dosen't have after data"); } + private IndexProto.IndexKey generateIndexKey(TableMetadata tableMetadata, Map rowValue) { + List keyColumnNames = tableMetadata.getKeyColumnNames(); + SinglePointIndex index = tableMetadata.getIndex(); + int len = keyColumnNames.size(); + List keyColumnValues = new ArrayList<>(len); + int keySize = 0; + for (String keyColumnName : keyColumnNames) { + ByteString value = rowValue.get(keyColumnName).getValue(); + keyColumnValues.add(value); + keySize += value.size(); + } - // TODO change - public RetinaProto.ColumnValue getBeforePk() { - return rowRecord.getBefore().getValues(0).getValue(); - } + ByteBuffer byteBuffer = ByteBuffer.allocate(keySize); + for (ByteString value : keyColumnValues) { + byteBuffer.put(value.toByteArray()); + } - public RetinaProto.ColumnValue getAfterPk() { - return rowRecord.getBefore().getValues(0).getValue(); + return IndexProto.IndexKey.newBuilder() + .setTimestamp(timeStamp) + .setKey(ByteString.copyFrom(byteBuffer.rewind())) + .setIndexId(index.getId()) + .setTableId(tableMetadata.getTable().getId()) + .build(); } public String getSourceTable() { @@ -159,8 +226,11 @@ public String getTable() { } public String getFullTableName() { - return getSchemaName() + "." + getTable(); + // 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(); } + // TODO(AntiO2): How to Map Schema Names Between Source DB and Pixels public String getSchemaName() { return rowRecord.getSource().getDb(); @@ -171,10 +241,6 @@ public boolean hasError() { return false; } - public SinkProto.ErrorInfo getErrorInfo() { - return rowRecord.getError(); - } - public String getDb() { return rowRecord.getSource().getDb(); } @@ -190,6 +256,7 @@ public boolean isInsert() { public boolean isSnapshot() { return getOp() == SinkProto.OperationType.SNAPSHOT; } + public boolean isUpdate() { return getOp() == SinkProto.OperationType.UPDATE; } @@ -202,14 +269,6 @@ public boolean hasAfterData() { return isUpdate() || isInsert() || isSnapshot(); } - public Long getTimeStampUs() { - return rowRecord.getTsUs(); - } - - public int getPkId() { - return tableMetadata.getPkId(); - } - public void startLatencyTimer() { this.latencyTimer = metricsFacade.startProcessLatencyTimer(); } @@ -225,11 +284,29 @@ public SinkProto.OperationType getOp() { return rowRecord.getOp(); } - public SinkProto.RowValue getBeforeData() { + public SinkProto.RowValue getBefore() { return rowRecord.getBefore(); } - public SinkProto.RowValue getAfterData() { + public SinkProto.RowValue getAfter() { return rowRecord.getAfter(); } + + public List getAfterData() { + List colValues = rowRecord.getAfter().getValuesList(); + List colValueList = new ArrayList<>(colValues.size()); + for (SinkProto.ColumnValue col : colValues) { + colValueList.add(col.getValue()); + } + return colValueList; + } + + @Override + public String toString() { + String sb = "RowChangeEvent{" + + rowRecord.getSource().getDb() + + "." + rowRecord.getSource().getTable() + + rowRecord.getTransaction().getId(); + return sb; + } } diff --git a/src/main/java/io/pixelsdb/pixels/sink/event/deserializer/DeserializerUtil.java b/src/main/java/io/pixelsdb/pixels/sink/event/deserializer/DeserializerUtil.java new file mode 100644 index 0000000..03fe0a7 --- /dev/null +++ b/src/main/java/io/pixelsdb/pixels/sink/event/deserializer/DeserializerUtil.java @@ -0,0 +1,96 @@ +/* + * 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.apache.kafka.connect.source.SourceRecord; + +public class DeserializerUtil { + static public SinkProto.TransactionStatus getStatusSafely(T record, String field) { + String statusString = getStringSafely(record, field); + if (statusString.equals("BEGIN")) { + return SinkProto.TransactionStatus.BEGIN; + } + if (statusString.equals("END")) { + return SinkProto.TransactionStatus.END; + } + + return SinkProto.TransactionStatus.UNRECOGNIZED; + } + + public static Object getFieldSafely(T record, String field) { + try { + if (record instanceof GenericRecord avro) { + return avro.get(field); + } else if (record instanceof Struct struct) { + return struct.get(field); + } else if (record instanceof SourceRecord sourceRecord) { + return ((Struct) sourceRecord.value()).get(field); + } + } catch (Exception e) { + return null; + } + return null; + } + + public static String getStringSafely(T record, String field) { + Object value = getFieldSafely(record, field); + return value != null ? value.toString() : ""; + } + + public static Long getLongSafely(T record, String field) { + Object value = getFieldSafely(record, field); + return value instanceof Number ? ((Number) value).longValue() : 0L; + } + + public static Integer getIntSafely(T record, String field) { + Object value = getFieldSafely(record, field); + return value instanceof Number ? ((Number) value).intValue() : 0; + } + + static public SinkProto.OperationType getOperationType(String op) { + op = op.toLowerCase(); + return switch (op) { + 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)); + }; + } + + static public boolean hasBeforeValue(SinkProto.OperationType op) { + return op == SinkProto.OperationType.DELETE || op == SinkProto.OperationType.UPDATE; + } + + static public 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/deserializer/RowChangeEventAvroDeserializer.java b/src/main/java/io/pixelsdb/pixels/sink/event/deserializer/RowChangeEventAvroDeserializer.java similarity index 70% rename from src/main/java/io/pixelsdb/pixels/sink/deserializer/RowChangeEventAvroDeserializer.java rename to src/main/java/io/pixelsdb/pixels/sink/event/deserializer/RowChangeEventAvroDeserializer.java index dc25482..81d9036 100644 --- a/src/main/java/io/pixelsdb/pixels/sink/deserializer/RowChangeEventAvroDeserializer.java +++ b/src/main/java/io/pixelsdb/pixels/sink/event/deserializer/RowChangeEventAvroDeserializer.java @@ -1,21 +1,24 @@ /* * 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 + * This file is part of Pixels. * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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. * - * 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. + * 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.deserializer; +package io.pixelsdb.pixels.sink.event.deserializer; import io.apicurio.registry.serde.SerdeConfig; import io.apicurio.registry.serde.avro.AvroKafkaDeserializer; @@ -24,8 +27,9 @@ 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.monitor.MetricsFacade; +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; @@ -67,12 +71,12 @@ private void registerSchema(String topic, Schema avroSchema) { } - private RowChangeEvent convertToRowChangeEvent(GenericRecord avroRecord, Schema schema) { + 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")); - +// .setTsMs(DeserializerUtil.getLongSafely(avroRecord, "ts_ms")); + ; if (avroRecord.get("source") != null) { //TODO: 这里看下怎么处理,如果没有source信息,其实可以通过topic推出schema和table信息。 parseSourceInfo((GenericRecord) avroRecord.get("source"), recordBuilder.getSourceBuilder()); @@ -80,7 +84,12 @@ private RowChangeEvent convertToRowChangeEvent(GenericRecord avroRecord, Schema String sourceSchema = recordBuilder.getSource().getDb(); String sourceTable = recordBuilder.getSource().getTable(); - TypeDescription typeDescription = tableMetadataRegistry.parseTypeDescription(avroRecord, sourceSchema, sourceTable); + 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)); @@ -113,23 +122,28 @@ private SinkProto.RowValue.Builder parseRowData(Object data, TypeDescription typ } private void parseSourceInfo(GenericRecord source, SinkProto.SourceInfo.Builder builder) { - builder.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")) + + builder .setDb(DeserializerUtil.getStringSafely(source, "db")) - .setSequence(DeserializerUtil.getStringSafely(source, "sequence")) .setSchema(DeserializerUtil.getStringSafely(source, "schema")) .setTable(DeserializerUtil.getStringSafely(source, "table")) - .setTxId(DeserializerUtil.getLongSafely(source, "tx_id")) - .setLsn(DeserializerUtil.getLongSafely(source, "lsn")) - .setXmin(DeserializerUtil.getLongSafely(source, "xmin")); +// .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.getStringSafely(transaction, "id")) + builder.setId(DeserializerUtil.getTransIdPrefix(DeserializerUtil.getStringSafely(transaction, "id"))) .setTotalOrder(DeserializerUtil.getLongSafely(transaction, "total_order")) .setDataCollectionOrder(DeserializerUtil.getLongSafely(transaction, "data_collection_order")); } 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 new file mode 100644 index 0000000..99c8677 --- /dev/null +++ b/src/main/java/io/pixelsdb/pixels/sink/event/deserializer/RowChangeEventJsonDeserializer.java @@ -0,0 +1,155 @@ +/* + * 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 new file mode 100644 index 0000000..7cd4ba1 --- /dev/null +++ b/src/main/java/io/pixelsdb/pixels/sink/event/deserializer/RowChangeEventStructDeserializer.java @@ -0,0 +1,133 @@ +/* + * 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/RowDataParser.java b/src/main/java/io/pixelsdb/pixels/sink/event/deserializer/RowDataParser.java new file mode 100644 index 0000000..63b7fa2 --- /dev/null +++ b/src/main/java/io/pixelsdb/pixels/sink/event/deserializer/RowDataParser.java @@ -0,0 +1,329 @@ +/* + * 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.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 { + private final TypeDescription schema; + + public RowDataParser(TypeDescription schema) { + this.schema = schema; + } + + + public void parse(GenericRecord record, SinkProto.RowValue.Builder builder) { + for (int i = 0; i < schema.getFieldNames().size(); i++) { + String fieldName = schema.getFieldNames().get(i); + TypeDescription fieldType = schema.getChildren().get(i); + builder.addValues(parseValue(record, fieldName, fieldType).build()); + } + } + + public void parse(JsonNode node, SinkProto.RowValue.Builder builder) { + for (int i = 0; i < schema.getFieldNames().size(); i++) { + String fieldName = schema.getFieldNames().get(i); + TypeDescription fieldType = schema.getChildren().get(i); + builder.addValues(parseValue(node.get(fieldName), fieldName, fieldType).build()); + } + } + + + 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()); + } + } + + private SinkProto.ColumnValue.Builder parseValue(JsonNode valueNode, String fieldName, TypeDescription type) { + if (valueNode == null || valueNode.isNull()) { + return SinkProto.ColumnValue.newBuilder() + // .setName(fieldName) + .setValue(ByteString.EMPTY); + } + + SinkProto.ColumnValue.Builder columnValueBuilder = SinkProto.ColumnValue.newBuilder(); + + switch (type.getCategory()) { + case INT: { + int value = valueNode.asInt(); + byte[] bytes = ByteBuffer.allocate(Integer.BYTES).putInt(value).array(); + columnValueBuilder.setValue(ByteString.copyFrom(bytes)); + // columnValueBuilder.setType(PixelsProto.Type.newBuilder().setKind(PixelsProto.Type.Kind.INT)); + break; + } + case LONG: { + long value = valueNode.asLong(); + byte[] bytes = ByteBuffer.allocate(Long.BYTES).putLong(value).array(); + columnValueBuilder.setValue(ByteString.copyFrom(bytes)); + // columnValueBuilder.setType(PixelsProto.Type.newBuilder().setKind(PixelsProto.Type.Kind.LONG)); + break; + } + case CHAR: { + String text = valueNode.asText(); + byte[] bytes = new byte[]{(byte) text.charAt(0)}; + columnValueBuilder.setValue(ByteString.copyFrom(bytes)); + // columnValueBuilder.setType(PixelsProto.Type.newBuilder() +// .setKind(PixelsProto.Type.Kind.STRING)); + break; + } + case VARCHAR: + case STRING: + case VARBINARY: { + String value = valueNode.asText().trim(); + columnValueBuilder.setValue(ByteString.copyFrom(value, StandardCharsets.UTF_8)); + // columnValueBuilder.setType(PixelsProto.Type.newBuilder().setKind(PixelsProto.Type.Kind.STRING)); + break; + } + case DECIMAL: { + String value = parseDecimal(valueNode, type).toString(); + columnValueBuilder.setValue(ByteString.copyFrom(value, StandardCharsets.UTF_8)); + // columnValueBuilder.setType(PixelsProto.Type.newBuilder() +// .setKind(PixelsProto.Type.Kind.DECIMAL) +// .setDimension(type.getPrecision()) +// .setScale(type.getScale())); + break; + } + case BINARY: { + String base64 = valueNode.asText(); // assume already base64 encoded + columnValueBuilder.setValue(ByteString.copyFrom(base64, StandardCharsets.UTF_8)); + // columnValueBuilder.setType(PixelsProto.Type.newBuilder().setKind(PixelsProto.Type.Kind.BINARY)); + break; + } + case STRUCT: { + // You can recursively parse fields in a struct here + throw new UnsupportedOperationException("STRUCT parsing not yet implemented"); + } + case DOUBLE: { + double value = valueNode.asDouble(); + long longBits = Double.doubleToLongBits(value); + byte[] bytes = ByteBuffer.allocate(Long.BYTES).putLong(longBits).array(); + columnValueBuilder.setValue(ByteString.copyFrom(bytes)); + // columnValueBuilder.setType(PixelsProto.Type.newBuilder().setKind(PixelsProto.Type.Kind.DOUBLE)); + break; + } + case FLOAT: { + float value = (float) valueNode.asDouble(); + int intBits = Float.floatToIntBits(value); + byte[] bytes = ByteBuffer.allocate(4).putInt(intBits).array(); + columnValueBuilder.setValue(ByteString.copyFrom(bytes)); + // columnValueBuilder.setType(PixelsProto.Type.newBuilder().setKind(PixelsProto.Type.Kind.FLOAT)); + break; + } + case DATE: { + int isoDate = valueNode.asInt(); + byte[] bytes = ByteBuffer.allocate(Integer.BYTES).putInt(isoDate).array(); + columnValueBuilder.setValue(ByteString.copyFrom(bytes)); + // columnValueBuilder.setType(PixelsProto.Type.newBuilder() + // .setKind(PixelsProto.Type.Kind.DATE)); + break; + } + case TIMESTAMP: { + long timestamp = valueNode.asLong(); + byte[] bytes = ByteBuffer.allocate(Long.BYTES).putLong(timestamp).array(); + columnValueBuilder.setValue(ByteString.copyFrom(bytes)); + // columnValueBuilder.setType(PixelsProto.Type.newBuilder() + // .setKind(PixelsProto.Type.Kind.DATE)); + break; + } + default: + throw new IllegalArgumentException("Unsupported type: " + type.getCategory()); + } + + return columnValueBuilder; + } + + + @Deprecated // TODO: use bit + private SinkProto.ColumnValue.Builder parseValue(GenericRecord record, String fieldName, TypeDescription fieldType) { + SinkProto.ColumnValue.Builder columnValueBuilder = SinkProto.ColumnValue.newBuilder(); + // columnValueBuilder.setName(fieldName); + + Object raw = record.get(fieldName); + if (raw == null) { + columnValueBuilder.setValue(ByteString.EMPTY); + return columnValueBuilder; + } + + switch (fieldType.getCategory()) { + 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)); + break; + } + + case LONG: { + long value = (long) raw; + columnValueBuilder.setValue(ByteString.copyFrom(Long.toString(value), StandardCharsets.UTF_8)); + // columnValueBuilder.setType(PixelsProto.Type.newBuilder().setKind(PixelsProto.Type.Kind.LONG)); + break; + } + + case STRING: { + String value = raw.toString(); + columnValueBuilder.setValue(ByteString.copyFrom(value, StandardCharsets.UTF_8)); + // columnValueBuilder.setType(PixelsProto.Type.newBuilder().setKind(PixelsProto.Type.Kind.STRING)); + 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)); + 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: + case INT32: { + int value = (Integer) record; + byte[] bytes = ByteBuffer.allocate(Integer.BYTES).putInt(value).array(); + columnValueBuilder.setValue(ByteString.copyFrom(bytes)); + // columnValueBuilder.setType(PixelsProto.Type.newBuilder().setKind(PixelsProto.Type.Kind.INT)); + break; + } + case INT64: { + 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)); + break; + } + case BYTES: { + byte[] bytes = (byte[]) record; + columnValueBuilder.setValue(ByteString.copyFrom(bytes)); + // columnValueBuilder.setType(PixelsProto.Type.newBuilder().setKind(PixelsProto.Type.Kind.BYTE)); + 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)); + 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; + int intBits = Float.floatToIntBits(value); + byte[] bytes = ByteBuffer.allocate(4).putInt(intBits).array(); + columnValueBuilder.setValue(ByteString.copyFrom(bytes)); + // columnValueBuilder.setType(PixelsProto.Type.newBuilder().setKind(PixelsProto.Type.Kind.FLOAT)); + break; + } + default: + throw new IllegalArgumentException("Unsupported type: " + type); + } + + return columnValueBuilder; + } + + private Map parseDeleteRecord() { + return Collections.singletonMap("__deleted", true); + } + + BigDecimal parseDecimal(JsonNode node, TypeDescription type) { + byte[] bytes = Base64.getDecoder().decode(node.asText()); + int scale = type.getScale(); + 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/deserializer/SchemaDeserializer.java b/src/main/java/io/pixelsdb/pixels/sink/event/deserializer/SchemaDeserializer.java similarity index 89% rename from src/main/java/io/pixelsdb/pixels/sink/deserializer/SchemaDeserializer.java rename to src/main/java/io/pixelsdb/pixels/sink/event/deserializer/SchemaDeserializer.java index 97e7802..3bf3959 100644 --- a/src/main/java/io/pixelsdb/pixels/sink/deserializer/SchemaDeserializer.java +++ b/src/main/java/io/pixelsdb/pixels/sink/event/deserializer/SchemaDeserializer.java @@ -1,21 +1,24 @@ /* * 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 + * This file is part of Pixels. * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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. * - * 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. + * 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.deserializer; +package io.pixelsdb.pixels.sink.event.deserializer; import com.fasterxml.jackson.databind.JsonNode; import io.pixelsdb.pixels.core.TypeDescription; @@ -45,9 +48,10 @@ private static JsonNode findSchemaField(JsonNode schemaNode, String targetField) return null; } - static TypeDescription parseStruct(JsonNode fields) { + public static TypeDescription parseStruct(JsonNode fields) { TypeDescription structType = TypeDescription.createStruct(); - fields.forEach(field -> { + fields.forEach(field -> + { String name = field.get("field").asText(); TypeDescription fieldType = parseFieldType(field); structType.addField(name, fieldType); diff --git a/src/main/java/io/pixelsdb/pixels/sink/deserializer/TransactionAvroMessageDeserializer.java b/src/main/java/io/pixelsdb/pixels/sink/event/deserializer/TransactionAvroMessageDeserializer.java similarity index 50% rename from src/main/java/io/pixelsdb/pixels/sink/deserializer/TransactionAvroMessageDeserializer.java rename to src/main/java/io/pixelsdb/pixels/sink/event/deserializer/TransactionAvroMessageDeserializer.java index db75449..3cd4a62 100644 --- a/src/main/java/io/pixelsdb/pixels/sink/deserializer/TransactionAvroMessageDeserializer.java +++ b/src/main/java/io/pixelsdb/pixels/sink/event/deserializer/TransactionAvroMessageDeserializer.java @@ -1,28 +1,31 @@ /* * 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 + * This file is part of Pixels. * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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. * - * 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. + * 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.deserializer; +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.monitor.MetricsFacade; +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; @@ -53,37 +56,13 @@ public SinkProto.TransactionMetadata deserialize(String topic, byte[] bytes) { try { MetricsFacade.getInstance().addRawData(bytes.length); GenericRecord avroRecord = avroDeserializer.deserialize(topic, bytes); - return convertToTransactionMetadata(avroRecord); + 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); } } - private SinkProto.TransactionMetadata convertToTransactionMetadata(GenericRecord record) { - SinkProto.TransactionMetadata.Builder builder = - SinkProto.TransactionMetadata.newBuilder(); - builder.setStatus(DeserializerUtil.getStatusSafely(record, "status")) - .setId(DeserializerUtil.getStringSafely(record, "id")) - .setEventCount(DeserializerUtil.getLongSafely(record, "event_count")) - .setTimestamp(DeserializerUtil.getLongSafely(record, "ts_ms")); - - if (record.get("data_collections") != null) { - Iterable collections = (Iterable) record.get("data_collections"); - for (Object item : 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); - } - } - } - - return builder.build(); - } - @Override public void close() { Deserializer.super.close(); diff --git a/src/main/java/io/pixelsdb/pixels/sink/deserializer/TransactionJsonMessageDeserializer.java b/src/main/java/io/pixelsdb/pixels/sink/event/deserializer/TransactionJsonMessageDeserializer.java similarity index 66% rename from src/main/java/io/pixelsdb/pixels/sink/deserializer/TransactionJsonMessageDeserializer.java rename to src/main/java/io/pixelsdb/pixels/sink/event/deserializer/TransactionJsonMessageDeserializer.java index 8923155..adc33eb 100644 --- a/src/main/java/io/pixelsdb/pixels/sink/deserializer/TransactionJsonMessageDeserializer.java +++ b/src/main/java/io/pixelsdb/pixels/sink/event/deserializer/TransactionJsonMessageDeserializer.java @@ -1,26 +1,29 @@ /* * 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 + * This file is part of Pixels. * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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. * - * 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. + * 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.deserializer; +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.monitor.MetricsFacade; +import io.pixelsdb.pixels.sink.util.MetricsFacade; import org.apache.kafka.common.serialization.Deserializer; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -54,6 +57,8 @@ private SinkProto.TransactionMetadata parseTransactionMetadata(Map. + */ + +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 new file mode 100644 index 0000000..aef64a1 --- /dev/null +++ b/src/main/java/io/pixelsdb/pixels/sink/exception/SinkException.java @@ -0,0 +1,35 @@ +/* + * 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.exception; + +public class SinkException extends Exception { + public SinkException(String msg) { + super(msg); + } + + public SinkException(String message, Throwable cause) { + super(message, cause); + } + + public SinkException(Throwable cause) { + super(cause); + } +} diff --git a/src/main/java/io/pixelsdb/pixels/sink/freshness/FreshnessClient.java b/src/main/java/io/pixelsdb/pixels/sink/freshness/FreshnessClient.java new file mode 100644 index 0000000..2ca9fef --- /dev/null +++ b/src/main/java/io/pixelsdb/pixels/sink/freshness/FreshnessClient.java @@ -0,0 +1,349 @@ +/* + * 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.freshness; + +import io.pixelsdb.pixels.common.exception.TransException; +import io.pixelsdb.pixels.common.transaction.TransContext; +import io.pixelsdb.pixels.common.transaction.TransService; +import io.pixelsdb.pixels.sink.config.PixelsSinkConfig; +import io.pixelsdb.pixels.sink.config.factory.PixelsSinkConfigFactory; +import io.pixelsdb.pixels.sink.util.DateUtil; +import io.pixelsdb.pixels.sink.util.MetricsFacade; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.sql.*; +import java.util.*; +import java.util.Date; +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. + */ +public class FreshnessClient { + private static final Logger LOGGER = LoggerFactory.getLogger(FreshnessClient.class); + 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 int maxConcurrentQueries; + private final Semaphore queryPermits; + private final ThreadPoolExecutor connectionExecutor; + // Key modification: Use a thread-safe Set to maintain the list of tables to monitor dynamically. + private final Set monitoredTables; + private final ScheduledExecutorService scheduler; + private final MetricsFacade metricsFacade = MetricsFacade.getInstance(); + private final int warmUpSeconds; + private final PixelsSinkConfig config; + + 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.warmUpSeconds = config.getSinkMonitorFreshnessEmbedWarmupSeconds(); + this.maxConcurrentQueries = config.getTrinoParallel(); + this.queryPermits = new Semaphore(maxConcurrentQueries); + // Initializes a single-threaded scheduler for executing freshness queries + this.scheduler = Executors.newSingleThreadScheduledExecutor(r -> + { + Thread t = Executors.defaultThreadFactory().newThread(r); + t.setName("Freshness-Client-Scheduler"); + t.setDaemon(true); + return t; + }); + + this.connectionExecutor = new ThreadPoolExecutor( + maxConcurrentQueries, + maxConcurrentQueries, + 60L, + TimeUnit.SECONDS, + new LinkedBlockingQueue<>(), + r -> + { + Thread t = Executors.defaultThreadFactory().newThread(r); + t.setName("Freshness-Query-Worker"); + t.setDaemon(true); + return t; + } + ); + this.connectionExecutor.allowCoreThreadTimeOut(true); + } + + public static FreshnessClient getInstance() { + if (instance == null) { + // First check: Reduces synchronization overhead once the instance is created + synchronized (FreshnessClient.class) { + if (instance == null) { + // Second check: Only one thread proceeds to create the instance + instance = new FreshnessClient(); + } + } + } + return instance; + } + + @Deprecated + protected Connection createNewConnection() throws SQLException { + try { + Class.forName("io.trino.jdbc.TrinoDriver"); + } catch (ClassNotFoundException e) { + throw new SQLException(e); + } + + Properties properties = new Properties(); + + + return DriverManager.getConnection(trinoJdbcUrl, trinoUser, null); + } + + protected Connection createNewConnection(long queryTimestamp) throws SQLException { + try { + Class.forName("io.trino.jdbc.TrinoDriver"); + } catch (ClassNotFoundException e) { + throw new SQLException(e); + } + + Properties properties = new Properties(); + properties.setProperty("user", trinoUser); + String catalogName = "pixels"; + String sessionPropValue = String.format("%s.query_snapshot_timestamp:%d", catalogName, queryTimestamp); + + properties.setProperty("sessionProperties", sessionPropValue); + return DriverManager.getConnection(trinoJdbcUrl, properties); + } + + private void closeConnection(Connection conn) { + if (conn != null) { + try { + conn.close(); + } catch (SQLException e) { + LOGGER.warn("Error closing Trino connection.", e); + } + } + } + + // ------------------------------------------------------------------------------------------------- + // Dynamic Table List Management + // ------------------------------------------------------------------------------------------------- + + /** + * Adds a table name to the monitoring list. + * This method can be called by external components (e.g., config trigger). + * + * @param tableName The name of the table to add. + */ + public void addMonitoredTable(String tableName) { + if (tableName == null || tableName.trim().isEmpty()) { + LOGGER.warn("Attempted to add null or empty table name to freshness monitor."); + return; + } + monitoredTables.add(tableName); + } + + /** + * Removes a table name from the monitoring list. + * + * @param tableName The name of the table to remove. + */ + public void removeMonitoredTable(String tableName) { + if (monitoredTables.remove(tableName)) { + LOGGER.info("Table '{}' removed from freshness monitor list.", tableName); + } else { + LOGGER.debug("Table '{}' was not found in the freshness monitor list.", tableName); + } + } + + // ------------------------------------------------------------------------------------------------- + // Scheduling and Execution + // ------------------------------------------------------------------------------------------------- + + /** + * Starts the scheduled freshness monitoring task. + */ + public void start() { + LOGGER.info("Starting Freshness Client, querying every {} seconds.", QUERY_INTERVAL_SECONDS); + scheduler.scheduleAtFixedRate(this::submitQueryTask, + warmUpSeconds, + QUERY_INTERVAL_SECONDS, + TimeUnit.SECONDS); + } + + + /** + * Stops the scheduled task and closes the JDBC connection. + */ + public void stop() { + LOGGER.info("Stopping Freshness Client."); + scheduler.shutdownNow(); + connectionExecutor.shutdownNow(); + } + + private void submitQueryTask() { + if (monitoredTables.isEmpty()) { + LOGGER.debug("No tables configured for freshness monitoring. Skipping submission cycle."); + return; + } + + if (!queryPermits.tryAcquire()) { + LOGGER.debug("Max concurrent queries ({}) reached. Skipping query submission this cycle.", maxConcurrentQueries); + return; + } + + try { + connectionExecutor.submit(this::queryAndCalculateFreshness); + } catch (RejectedExecutionException e) { + queryPermits.release(); + LOGGER.error("Query task rejected by executor. Max concurrent queries may be too low or service is stopping.", e); + } catch (Exception e) { + queryPermits.release(); + LOGGER.error("Unknown error during task submission.", e); + } + } + + /** + * The core scheduled task: queries max(freshness_ts) for all monitored tables + * and calculates the freshness metric. + */ + void queryAndCalculateFreshness() { + Connection conn = null; + TransContext transContext = null; + + String tableName; + try { + + tableName = getRandomTable(); + if (tableName == null) { + return; + } + + LOGGER.debug("Randomly selected table for this cycle: {}", tableName); + // Timestamp when the query is sent (t_send) + long tSendMillis = System.currentTimeMillis(); + if (config.isSinkMonitorFreshnessEmbedSnapshot()) { + transContext = TransService.Instance().beginTrans(true); + conn = createNewConnection(transContext.getTimestamp()); + } else { + conn = createNewConnection(); + } + + String tSendMillisStr = DateUtil.convertDateToString(new Date(tSendMillis)); + // Query to find the latest timestamp in the table + // Assumes 'freshness_ts' is a long-type epoch timestamp (milliseconds) + String query = String.format("SELECT max(freshness_ts) FROM %s WHERE freshness_ts < TIMESTAMP '%s'", tableName, tSendMillisStr); + + try (Statement statement = conn.createStatement(); + ResultSet rs = statement.executeQuery(query)) { + + Timestamp maxFreshnessTs = null; + + if (rs.next()) { + // Read the maximum timestamp value + maxFreshnessTs = rs.getTimestamp(1); + } + + if (maxFreshnessTs != null) { + // Freshness = t_send - data_write_time (maxFreshnessTs) + // Result is in milliseconds + long freshnessMillis = tSendMillis - maxFreshnessTs.getTime(); + metricsFacade.recordTableFreshness(tableName, freshnessMillis); + } else { + LOGGER.warn("Table {} returned null or zero max(freshness_ts). Skipping freshness calculation.", tableName); + } + + } catch (SQLException e) { + // Handle database errors (e.g., table not found, query syntax error) + LOGGER.error("Failed to execute query for table {}: {}", tableName, e.getMessage()); + } catch (Exception e) { + // Catch potential runtime errors (e.g., in MetricsFacade) + LOGGER.error("Error calculating or recording freshness for table {}.", tableName, e); + } + } catch (Exception e) { + LOGGER.error("Error selecting a random table from the monitor list.", e); + } finally { + if (config.isSinkMonitorFreshnessEmbedSnapshot() && transContext != null) { + try { + TransService.Instance().commitTrans(transContext.getTransId(), true); + } catch (TransException e) { + throw new RuntimeException(e); + } + } + closeConnection(conn); + queryPermits.release(); + } + + } + + + private String getRandomTable() { + List tableList; + if (config.isSinkMonitorFreshnessEmbedStatic()) { + tableList = getStaticList(); + } else { + tableList = getDynamicList(); + } + + if (tableList == null || tableList.isEmpty()) { + return null; + } + + Random random = new Random(); + int randomIndex = random.nextInt(tableList.size()); + + return tableList.get(randomIndex); + } + + private List getDynamicList() { + // Take a snapshot of the tables to monitor for this cycle. + // This prevents ConcurrentModificationException if a table is added/removed mid-iteration. + Set tablesSnapshot = new HashSet<>(monitoredTables); + + if (tablesSnapshot.isEmpty()) { + LOGGER.debug("No tables configured for freshness monitoring. Skipping cycle."); + return null; + } + + monitoredTables.clear(); + + List staticList = getStaticList(); + + // If staticList is empty or null, return all tablesSnapshot + if (staticList == null || staticList.isEmpty()) { + return new ArrayList<>(tablesSnapshot); + } + + // Return intersection of tablesSnapshot and staticList + Set staticSet = new HashSet<>(staticList); + tablesSnapshot.retainAll(staticSet); + + return new ArrayList<>(tablesSnapshot); + } + + private List getStaticList() { + return config.getSinkMonitorFreshnessEmbedTableList(); + } +} \ No newline at end of file diff --git a/src/main/java/io/pixelsdb/pixels/sink/freshness/FreshnessHistory.java b/src/main/java/io/pixelsdb/pixels/sink/freshness/FreshnessHistory.java new file mode 100644 index 0000000..613724d --- /dev/null +++ b/src/main/java/io/pixelsdb/pixels/sink/freshness/FreshnessHistory.java @@ -0,0 +1,55 @@ +/* + * 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.freshness; + + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.ConcurrentLinkedQueue; + +public class FreshnessHistory { + private final ConcurrentLinkedQueue history = new ConcurrentLinkedQueue<>(); + + public void record(double freshnessMill) { + history.offer(new Record(System.currentTimeMillis(), freshnessMill)); + } + + public List pollAll() { + if (history.isEmpty()) { + return Collections.emptyList(); + } + List records = new ArrayList<>(); + Record record; + while ((record = history.poll()) != null) { + records.add(record); + } + return records; + } + + public record Record(long timestamp, double value) { + + @Override + public String toString() { + return timestamp + "," + value; + } + } +} \ No newline at end of file diff --git a/src/main/java/io/pixelsdb/pixels/sink/freshness/OneSecondAverage.java b/src/main/java/io/pixelsdb/pixels/sink/freshness/OneSecondAverage.java new file mode 100644 index 0000000..c2aeb49 --- /dev/null +++ b/src/main/java/io/pixelsdb/pixels/sink/freshness/OneSecondAverage.java @@ -0,0 +1,85 @@ +/* + * 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.freshness; + +import java.util.ArrayDeque; +import java.util.Deque; + +public class OneSecondAverage { + /** + * Time window in milliseconds + */ + private final int windowMillis; + + /** + * Sliding window storing timestamped values + */ + private final Deque window = new ArrayDeque<>(); + + /** + * Constructor with configurable window size (milliseconds) + */ + public OneSecondAverage(int windowMillis) { + this.windowMillis = windowMillis; + } + + /** + * Record a new data point + */ + public synchronized void record(double v) { + long now = System.currentTimeMillis(); + window.addLast(new TimedValue(now, v)); + evictOld(now); + } + + /** + * Remove all values older than windowMillis + */ + private void evictOld(long now) { + while (!window.isEmpty() && now - window.peekFirst().timestamp > windowMillis) { + window.removeFirst(); + } + } + + /** + * Compute average of values in the time window + */ + public synchronized double getWindowAverage() { + long now = System.currentTimeMillis(); + evictOld(now); + + if (window.isEmpty()) { + return Double.NaN; + } + + double sum = 0; + for (TimedValue tv : window) { + sum += tv.value; + } + return sum / window.size(); + } + + /** + * Timestamped data point + */ + private record TimedValue(long timestamp, double value) { + } +} diff --git a/src/main/java/io/pixelsdb/pixels/sink/metadata/SchemaCache.java b/src/main/java/io/pixelsdb/pixels/sink/metadata/SchemaCache.java deleted file mode 100644 index 8d7bef9..0000000 --- a/src/main/java/io/pixelsdb/pixels/sink/metadata/SchemaCache.java +++ /dev/null @@ -1,46 +0,0 @@ -/* - * 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.metadata; - -import io.pixelsdb.pixels.core.TypeDescription; - -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.ConcurrentMap; -import java.util.function.Supplier; - -public class SchemaCache { - private static final SchemaCache INSTANCE = new SchemaCache(); - private final ConcurrentMap cache = new ConcurrentHashMap<>(); - - private SchemaCache() { - } - - ; - - public static SchemaCache getInstance() { - return INSTANCE; - } - - public TypeDescription computeIfAbsent(TableMetadataKey tableMetadataKey, Supplier supplier) { - return cache.computeIfAbsent(tableMetadataKey, k -> supplier.get()); - } - - // public TypeDescription get(String topic) { -// return cache.get(topic); -// } -} \ No newline at end of file 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 87fe8ac..510dced 100644 --- a/src/main/java/io/pixelsdb/pixels/sink/metadata/TableMetadata.java +++ b/src/main/java/io/pixelsdb/pixels/sink/metadata/TableMetadata.java @@ -1,50 +1,85 @@ /* * 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 + * This file is part of Pixels. * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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. * - * 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. + * 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.metadata; +import io.pixelsdb.pixels.common.exception.MetadataException; import io.pixelsdb.pixels.common.metadata.domain.Column; -import io.pixelsdb.pixels.common.metadata.domain.SecondaryIndex; +import io.pixelsdb.pixels.common.metadata.domain.SinglePointIndex; import io.pixelsdb.pixels.common.metadata.domain.Table; +import io.pixelsdb.pixels.core.TypeDescription; import lombok.Getter; +import java.util.HashMap; import java.util.LinkedList; import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; @Getter public class TableMetadata { private final Table table; - private final SecondaryIndex index; - private final List keyColumnNames; - + private final SinglePointIndex index; + private final TypeDescription typeDescription; private final List columns; + private final List keyColumnNames; - public TableMetadata(Table table, SecondaryIndex index, List columns) { + public TableMetadata(Table table, SinglePointIndex index, List columns) throws MetadataException { this.table = table; this.index = index; this.columns = columns; - keyColumnNames = new LinkedList<>(); - List keyColumnIds = index.getKeyColumns().getKeyColumnIds(); - for (Integer keyColumnId : keyColumnIds) { - keyColumnNames.add(columns.get(keyColumnId).getName()); + this.keyColumnNames = new LinkedList<>(); + List columnNames = columns.stream().map(Column::getName).collect(Collectors.toList()); + List columnTypes = columns.stream().map(Column::getType).collect(Collectors.toList()); + typeDescription = TypeDescription.createSchemaFromStrings(columnNames, columnTypes); + if (index != null) { + Map columnMap = new HashMap<>(); + for (Column column : columns) { + columnMap.put(column.getId(), column); + } + + for (Integer keyColumnId : index.getKeyColumns().getKeyColumnIds()) { + Column column = columnMap.get(keyColumnId.longValue()); + if (column != null) { + keyColumnNames.add(column.getName()); + } else { + throw new MetadataException("Cant find key column id: " + keyColumnId + " in table " + + table.getName() + " schema id is " + table.getSchemaId()); + } + } } } - public int getPkId() { - return index.getKeyColumns().getKeyColumnIds().get(0); + public boolean hasPrimaryIndex() { + return index != null; + } + + public long getPrimaryIndexKeyId() { + return index.getId(); + } + + public long getTableId() { + return table.getId(); + } + + public long getSchemaId() { + return table.getSchemaId(); } } \ No newline at end of file diff --git a/src/main/java/io/pixelsdb/pixels/sink/metadata/TableMetadataKey.java b/src/main/java/io/pixelsdb/pixels/sink/metadata/TableMetadataKey.java deleted file mode 100644 index 3cb3b7d..0000000 --- a/src/main/java/io/pixelsdb/pixels/sink/metadata/TableMetadataKey.java +++ /dev/null @@ -1,50 +0,0 @@ -/* - * 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.metadata; - -import lombok.Getter; - -import java.util.Objects; - -public final class TableMetadataKey { - @Getter - private final String schemaName; - @Getter - private final String tableName; - private final int hash; - - public TableMetadataKey(String schemaName, String tableName) { - this.schemaName = schemaName.toLowerCase(); - this.tableName = tableName.toLowerCase(); - this.hash = Objects.hash(this.schemaName, this.tableName); - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - TableMetadataKey that = (TableMetadataKey) o; - return schemaName.equals(that.schemaName) && - tableName.equals(that.tableName); - } - - @Override - public int hashCode() { - return hash; - } -} 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 1d9b6b6..c754e8e 100644 --- a/src/main/java/io/pixelsdb/pixels/sink/metadata/TableMetadataRegistry.java +++ b/src/main/java/io/pixelsdb/pixels/sink/metadata/TableMetadataRegistry.java @@ -1,43 +1,50 @@ /* * 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 + * This file is part of Pixels. * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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. * - * 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. + * 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.metadata; import io.pixelsdb.pixels.common.exception.MetadataException; import io.pixelsdb.pixels.common.metadata.MetadataService; +import io.pixelsdb.pixels.common.metadata.SchemaTableName; import io.pixelsdb.pixels.common.metadata.domain.Column; -import io.pixelsdb.pixels.common.metadata.domain.SecondaryIndex; +import io.pixelsdb.pixels.common.metadata.domain.Schema; +import io.pixelsdb.pixels.common.metadata.domain.SinglePointIndex; import io.pixelsdb.pixels.common.metadata.domain.Table; import io.pixelsdb.pixels.core.TypeDescription; -import io.pixelsdb.pixels.sink.deserializer.SchemaDeserializer; -import org.apache.avro.Schema; -import org.apache.avro.generic.GenericData; -import org.apache.avro.generic.GenericRecord; +import io.pixelsdb.pixels.sink.exception.SinkException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import java.util.List; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; public class TableMetadataRegistry { + + private static final Logger logger = LoggerFactory.getLogger(TableMetadataRegistry.class); private static final MetadataService metadataService = MetadataService.Instance(); private static volatile TableMetadataRegistry instance; - private final ConcurrentMap registry = new ConcurrentHashMap<>(); - private final ConcurrentMap typeDescriptionConcurrentMap = new ConcurrentHashMap<>(); - private final SchemaCache schemaCache = SchemaCache.getInstance(); + + private final ConcurrentMap registry = new ConcurrentHashMap<>(); + private final ConcurrentMap tableId2SchemaTableName = new ConcurrentHashMap<>(); + private List schemas; private TableMetadataRegistry() { } @@ -53,45 +60,85 @@ public static TableMetadataRegistry Instance() { return instance; } - public TableMetadata getMetadata(String schema, String table) { - TableMetadataKey key = new TableMetadataKey(schema, table); - return registry.computeIfAbsent(key, k -> loadTableMetadata(schema, table)); + public TableMetadata getMetadata(String schema, String table) throws SinkException { + SchemaTableName key = new SchemaTableName(schema, table); + if (!registry.containsKey(key)) { + logger.debug("Registry doesn't contain {}", key); + TableMetadata metadata = loadTableMetadata(schema, table); + registry.put(key, metadata); + } + return registry.get(key); + } + + + public SchemaTableName getSchemaTableName(long tableId) throws SinkException { + if (!tableId2SchemaTableName.containsKey(tableId)) { + logger.info("SchemaTableName doesn't contain {}", tableId); + SchemaTableName metadata = loadSchemaTableName(tableId); + tableId2SchemaTableName.put(tableId, metadata); + } + return tableId2SchemaTableName.get(tableId); + } + + public TypeDescription getTypeDescription(String schemaName, String tableName) throws SinkException { + return getMetadata(schemaName, tableName).getTypeDescription(); + } + + public List getKeyColumnsName(String schemaName, String tableName) throws SinkException { + return getMetadata(schemaName, tableName).getKeyColumnNames(); + } + + public long getPrimaryIndexKeyId(String schemaName, String tableName) throws SinkException { + return getMetadata(schemaName, tableName).getPrimaryIndexKeyId(); + } + + + public long getTableId(String schemaName, String tableName) throws SinkException { + return getMetadata(schemaName, tableName).getTableId(); } - public TableMetadata loadTableMetadata(String schemaName, String tableName) { + + private TableMetadata loadTableMetadata(String schemaName, String tableName) throws SinkException { try { + logger.info("Metadata Cache miss: {} {}", schemaName, tableName); Table table = metadataService.getTable(schemaName, tableName); - SecondaryIndex index = metadataService.getSecondaryIndex(table.getId()); - /* - TODO(Lizn): we only use unique index? - */ + SinglePointIndex index = null; + try { + index = metadataService.getPrimaryIndex(table.getId()); + } catch (MetadataException e) { + logger.warn("Could not get primary index for table {}", tableName, e); + } + if (!index.isUnique()) { - throw new MetadataException("Non Unique Index is not supported"); + throw new MetadataException("Non Unique Index is not supported, Schema:" + schemaName + " Table: " + tableName); } List tableColumns = metadataService.getColumns(schemaName, tableName, false); return new TableMetadata(table, index, tableColumns); } catch (MetadataException e) { - throw new RuntimeException(e); + throw new SinkException(e); } } - public TypeDescription getTypeDescription(String schema, String table) { - return typeDescriptionConcurrentMap.get(new TableMetadataKey(schema, table)); - } + private SchemaTableName loadSchemaTableName(long tableId) throws SinkException { + // metadataService + try { + if (schemas == null) { + schemas = metadataService.getSchemas(); + } + Table table = metadataService.getTableById(tableId); + + long schemaId = table.getSchemaId(); + + Schema schema = schemas.stream() + .filter(s -> s.getId() == schemaId) + .findFirst() + .orElseThrow(() -> new MetadataException("Schema not found for id: " + schemaId)); + + return new SchemaTableName(table.getName(), schema.getName()); - /** - * parse typeDescription from avro record and cache it. - * - * @param record - * @return - */ - public TypeDescription parseTypeDescription(GenericRecord record, String sourceSchema, String sourceTable) { - Schema schema = ((GenericData.Record) record).getSchema().getField("before").schema().getTypes().get(1); - TableMetadataKey tableMetadataKey = new TableMetadataKey(sourceSchema, sourceTable); - TypeDescription typeDescription = typeDescriptionConcurrentMap.computeIfAbsent( - tableMetadataKey, - key -> SchemaDeserializer.parseFromAvroSchema(schema) - ); - return typeDescription; + } catch (MetadataException e) { + throw new SinkException(e); + } } + } diff --git a/src/main/java/io/pixelsdb/pixels/sink/monitor/MetricsFacade.java b/src/main/java/io/pixelsdb/pixels/sink/monitor/MetricsFacade.java deleted file mode 100644 index 8c02db3..0000000 --- a/src/main/java/io/pixelsdb/pixels/sink/monitor/MetricsFacade.java +++ /dev/null @@ -1,196 +0,0 @@ -/* - * 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.monitor; - -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.prometheus.client.Counter; -import io.prometheus.client.Summary; - -public class MetricsFacade { - private static MetricsFacade instance; - private final boolean enabled; - private final Counter tableChangeCounter; - private final Counter rowChangeCounter; - private final Counter transactionCounter; - private final Summary processingLatency; - private final Counter rawDataThroughputCounter; - - private final Summary transServiceLatency; - private final Summary indexServiceLatency; - private final Summary retinaServiceLatency; - private final Summary writerLatency; - private final Summary totalLatency; - private MetricsFacade(boolean enabled) { - this.enabled = enabled; - if (enabled) { - this.tableChangeCounter = Counter.build() - .name("sink_table_changes_total") - .help("Total processed table changes") - .labelNames("table") - .register(); - - this.rowChangeCounter = Counter.build() - .name("sink_row_changes_total") - .help("Total processed row changes") - .labelNames("table", "operation") - .register(); - - this.transactionCounter = Counter.build() - .name("sink_transactions_total") - .help("Total committed transactions") - .register(); - - this.processingLatency = Summary.build() - .name("sink_processing_latency_seconds") - .help("End-to-end processing latency") - .quantile(0.5, 0.05) - .quantile(0.75, 0.01) - .quantile(0.95, 0.005) - .quantile(0.99, 0.001) - .register(); - - this.rawDataThroughputCounter = Counter.build() - .name("sink_data_throughput_counter") - .help("Data throughput") - .register(); - - this.transServiceLatency = Summary.build() - .name("trans_service_latency_seconds") - .help("End-to-end processing latency") - .quantile(0.5, 0.05) - .quantile(0.75, 0.01) - .quantile(0.95, 0.005) - .quantile(0.99, 0.001) - .register(); - - this.indexServiceLatency = Summary.build() - .name("index_service_latency_seconds") - .help("End-to-end processing latency") - .quantile(0.5, 0.05) - .quantile(0.75, 0.01) - .quantile(0.95, 0.005) - .quantile(0.99, 0.001) - .register(); - - this.retinaServiceLatency = Summary.build() - .name("retina_service_latency_seconds") - .help("End-to-end processing latency") - .quantile(0.5, 0.05) - .quantile(0.75, 0.01) - .quantile(0.95, 0.005) - .quantile(0.99, 0.001) - .register(); - - this.writerLatency = Summary.build() - .name("write_latency_seconds") - .help("Write latency") - .quantile(0.5, 0.05) - .quantile(0.75, 0.01) - .quantile(0.95, 0.005) - .quantile(0.99, 0.001) - .register(); - - this.totalLatency = Summary.build() - .name("total_latency_seconds") - .help("total latency to ETL a row change event") - .labelNames("table", "operation") - .quantile(0.5, 0.05) - .quantile(0.75, 0.01) - .quantile(0.95, 0.005) - .quantile(0.99, 0.001) - .register(); - - } else { - this.rowChangeCounter = null; - this.transactionCounter = null; - this.processingLatency = null; - this.tableChangeCounter = null; - this.rawDataThroughputCounter = null; - this.transServiceLatency = null; - this.indexServiceLatency = null; - this.retinaServiceLatency = null; - this.writerLatency = null; - this.totalLatency = null; - } - } - - public static synchronized void initialize() { - PixelsSinkConfig config = PixelsSinkConfigFactory.getInstance(); - if (instance == null) { - instance = new MetricsFacade(config.isMonitorEnabled()); - } - } - - public static MetricsFacade getInstance() { - if (instance == null) { - initialize(); - } - return instance; - } - - public void recordRowChange(String table, SinkProto.OperationType operation) { - recordRowChange(table, operation, 1); - } - - public void recordRowChange(String table, SinkProto.OperationType operation, int rows) { - if (enabled && rowChangeCounter != null) { - tableChangeCounter.labels(table).inc(rows); - rowChangeCounter.labels(table, operation.toString()).inc(rows); - } - } - - public void recordTransaction() { - if (enabled && transactionCounter != null) { - transactionCounter.inc(); - } - } - - public Summary.Timer startProcessLatencyTimer() { - return enabled ? processingLatency.startTimer() : null; - } - - public Summary.Timer startIndexLatencyTimer() { - return enabled ? indexServiceLatency.startTimer() : null; - } - - public Summary.Timer startTransLatencyTimer() { - return enabled ? transServiceLatency.startTimer() : null; - } - - public Summary.Timer startRetinaLatencyTimer() { - return enabled ? retinaServiceLatency.startTimer() : null; - } - - public Summary.Timer startWriteLatencyTimer() { - return enabled ? writerLatency.startTimer() : null; - } - - public void addRawData(double data) { - rawDataThroughputCounter.inc(data); - } - - public void recordTotalLatency(RowChangeEvent event) { - if(event.getTimeStamp() != 0) { - long recordLatency = System.currentTimeMillis()- event.getTimeStamp(); - totalLatency.labels(event.getFullTableName(), event.getOp().toString()).observe(recordLatency); - } - } -} \ No newline at end of file diff --git a/src/main/java/io/pixelsdb/pixels/sink/monitor/SinkMonitor.java b/src/main/java/io/pixelsdb/pixels/sink/monitor/SinkMonitor.java deleted file mode 100644 index 5e0dd95..0000000 --- a/src/main/java/io/pixelsdb/pixels/sink/monitor/SinkMonitor.java +++ /dev/null @@ -1,74 +0,0 @@ -/* - * 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.monitor; - -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.prometheus.client.exporter.HTTPServer; - -import java.io.IOException; -import java.util.Properties; - -public class SinkMonitor implements StoppableMonitor { - private MonitorThreadManager manager; - private volatile boolean running = true; - private HTTPServer prometheusHttpServer; - public void startSinkMonitor() { - PixelsSinkConfig pixelsSinkConfig = PixelsSinkConfigFactory.getInstance(); - KafkaPropFactorySelector kafkaPropFactorySelector = new KafkaPropFactorySelector(); - - Properties transactionKafkaProperties = kafkaPropFactorySelector - .getFactory(PixelsSinkConstants.TRANSACTION_KAFKA_PROP_FACTORY) - .createKafkaProperties(pixelsSinkConfig); - TransactionMonitor transactionMonitor = new TransactionMonitor(pixelsSinkConfig, transactionKafkaProperties); - - Properties topicKafkaProperties = kafkaPropFactorySelector - .getFactory(PixelsSinkConstants.ROW_RECORD_KAFKA_PROP_FACTORY) - .createKafkaProperties(pixelsSinkConfig); - TopicMonitor topicMonitor = new TopicMonitor(pixelsSinkConfig, topicKafkaProperties); - - manager = new MonitorThreadManager(); - manager.startMonitor(transactionMonitor); - manager.startMonitor(topicMonitor); - - try { - if (pixelsSinkConfig.isMonitorEnabled()) { - this.prometheusHttpServer = new HTTPServer(PixelsSinkConfigFactory.getInstance().getMonitorPort()); - } - } catch (IOException e) { - throw new RuntimeException(e); - } - } - - - @Override - public void stopMonitor() { - manager.shutdown(); - if (prometheusHttpServer != null) { - prometheusHttpServer.close(); - } - - running = false; - } - - public boolean isRunning() { - return running; - } -} diff --git a/src/main/java/io/pixelsdb/pixels/sink/monitor/StoppableMonitor.java b/src/main/java/io/pixelsdb/pixels/sink/monitor/StoppableMonitor.java deleted file mode 100644 index 13686ab..0000000 --- a/src/main/java/io/pixelsdb/pixels/sink/monitor/StoppableMonitor.java +++ /dev/null @@ -1,23 +0,0 @@ -/* - * 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.monitor; - -public interface StoppableMonitor { - - void stopMonitor(); -} diff --git a/src/main/java/io/pixelsdb/pixels/sink/monitor/TransactionMonitor.java b/src/main/java/io/pixelsdb/pixels/sink/monitor/TransactionMonitor.java deleted file mode 100644 index bd2f3a0..0000000 --- a/src/main/java/io/pixelsdb/pixels/sink/monitor/TransactionMonitor.java +++ /dev/null @@ -1,95 +0,0 @@ -/* - * 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.monitor; - -import io.pixelsdb.pixels.sink.SinkProto; -import io.pixelsdb.pixels.sink.concurrent.TransactionCoordinator; -import io.pixelsdb.pixels.sink.concurrent.TransactionCoordinatorFactory; -import io.pixelsdb.pixels.sink.config.PixelsSinkConfig; -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 class TransactionMonitor implements Runnable, StoppableMonitor { - private static final Logger LOGGER = LoggerFactory.getLogger(TransactionMonitor.class); - - private final String transactionTopic; - private final KafkaConsumer consumer; - private final TransactionCoordinator transactionCoordinator; - private final AtomicBoolean running = new AtomicBoolean(true); - - public TransactionMonitor(PixelsSinkConfig pixelsSinkConfig, Properties kafkaProperties) { - this.transactionTopic = pixelsSinkConfig.getTopicPrefix() + "." + pixelsSinkConfig.getTransactionTopicSuffix(); - this.consumer = new KafkaConsumer<>(kafkaProperties); - this.transactionCoordinator = TransactionCoordinatorFactory.getCoordinator(); - } - - @Override - public void run() { - try { - consumer.subscribe(Collections.singletonList(transactionTopic)); - LOGGER.info("Started transaction monitor for topic: {}", transactionTopic); - - while (running.get()) { - try { - ConsumerRecords records = - consumer.poll(Duration.ofMillis(1000)); - - for (ConsumerRecord record : records) { - SinkProto.TransactionMetadata transaction = record.value(); - LOGGER.debug("Processing transaction event: {}", transaction.getId()); - transactionCoordinator.processTransactionEvent(transaction); - } - } catch (WakeupException e) { - if (running.get()) { - LOGGER.warn("Consumer wakeup unexpectedly", e); - } - } - } - } finally { - closeResources(); - LOGGER.info("Transaction monitor stopped"); - } - } - - @Override - public void stopMonitor() { - LOGGER.info("Stopping transaction monitor"); - running.set(false); - consumer.wakeup(); - } - - private void closeResources() { - try { - if (consumer != null) { - consumer.close(Duration.ofSeconds(5)); - LOGGER.debug("Kafka consumer closed"); - } - } catch (Exception e) { - LOGGER.warn("Error closing Kafka consumer", e); - } - } -} diff --git a/src/main/java/io/pixelsdb/pixels/sink/monitor/MonitorThreadManager.java b/src/main/java/io/pixelsdb/pixels/sink/processor/MonitorThreadManager.java similarity index 56% rename from src/main/java/io/pixelsdb/pixels/sink/monitor/MonitorThreadManager.java rename to src/main/java/io/pixelsdb/pixels/sink/processor/MonitorThreadManager.java index 0d25777..4b39a9a 100644 --- a/src/main/java/io/pixelsdb/pixels/sink/monitor/MonitorThreadManager.java +++ b/src/main/java/io/pixelsdb/pixels/sink/processor/MonitorThreadManager.java @@ -1,21 +1,24 @@ /* * 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 + * This file is part of Pixels. * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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. * - * 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. + * 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.monitor; +package io.pixelsdb.pixels.sink.processor; import io.pixelsdb.pixels.sink.config.PixelsSinkConstants; @@ -24,6 +27,7 @@ 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); @@ -40,9 +44,10 @@ public void shutdown() { } private void stopMonitors() { - monitors.forEach(monitor -> { - if (monitor instanceof StoppableMonitor) { - ((StoppableMonitor) monitor).stopMonitor(); + monitors.forEach(monitor -> + { + if (monitor instanceof StoppableProcessor) { + ((StoppableProcessor) monitor).stopProcessor(); } }); } diff --git a/src/main/java/io/pixelsdb/pixels/sink/processor/StoppableProcessor.java b/src/main/java/io/pixelsdb/pixels/sink/processor/StoppableProcessor.java new file mode 100644 index 0000000..4e6beff --- /dev/null +++ b/src/main/java/io/pixelsdb/pixels/sink/processor/StoppableProcessor.java @@ -0,0 +1,26 @@ +/* + * 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 new file mode 100644 index 0000000..1d31a28 --- /dev/null +++ b/src/main/java/io/pixelsdb/pixels/sink/processor/TableProcessor.java @@ -0,0 +1,77 @@ +/* + * 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.event.RowChangeEvent; +import io.pixelsdb.pixels.sink.provider.TableEventProvider; +import io.pixelsdb.pixels.sink.util.MetricsFacade; +import io.pixelsdb.pixels.sink.writer.PixelsSinkWriter; +import io.pixelsdb.pixels.sink.writer.PixelsSinkWriterFactory; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.concurrent.atomic.AtomicBoolean; + +/** + * @package: io.pixelsdb.pixels.sink.processor + * @className: TableProcessor + * @author: AntiO2 + * @date: 2025/9/26 11:01 + */ +public class TableProcessor implements StoppableProcessor, 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 MetricsFacade metricsFacade = MetricsFacade.getInstance(); + private Thread processorThread; + private boolean tableAdded = false; + + public TableProcessor(TableEventProvider tableEventProvider) { + this.pixelsSinkWriter = PixelsSinkWriterFactory.getWriter(); + this.tableEventProvider = tableEventProvider; + } + + @Override + public void run() { + processorThread = new Thread(this::processLoop); + processorThread.start(); + } + + private void processLoop() { + while (running.get()) { + RowChangeEvent event = tableEventProvider.getRowChangeEvent(); + if (event == null) { + continue; + } + pixelsSinkWriter.writeRow(event); + } + LOGGER.info("Processor thread exited"); + } + + @Override + public void stopProcessor() { + LOGGER.info("Stopping transaction monitor"); + running.set(false); + processorThread.interrupt(); + } +} diff --git a/src/main/java/io/pixelsdb/pixels/sink/monitor/TopicMonitor.java b/src/main/java/io/pixelsdb/pixels/sink/processor/TopicProcessor.java similarity index 80% rename from src/main/java/io/pixelsdb/pixels/sink/monitor/TopicMonitor.java rename to src/main/java/io/pixelsdb/pixels/sink/processor/TopicProcessor.java index 7f5eb73..1507f2d 100644 --- a/src/main/java/io/pixelsdb/pixels/sink/monitor/TopicMonitor.java +++ b/src/main/java/io/pixelsdb/pixels/sink/processor/TopicProcessor.java @@ -1,24 +1,27 @@ /* * 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 + * This file is part of Pixels. * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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. * - * 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. + * 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.monitor; +package io.pixelsdb.pixels.sink.processor; import io.pixelsdb.pixels.sink.config.PixelsSinkConfig; -import io.pixelsdb.pixels.sink.consumer.TableConsumerTask; +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; @@ -32,30 +35,28 @@ import java.util.concurrent.atomic.AtomicBoolean; import java.util.stream.Collectors; -public class TopicMonitor extends Thread implements StoppableMonitor { +public class TopicProcessor implements StoppableProcessor, Runnable { - private static final Logger log = LoggerFactory.getLogger(TopicMonitor.class); + 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 ExecutorService executorService; 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; - private final Map activeTasks = new ConcurrentHashMap<>(); // track row event consumer - - public TopicMonitor(PixelsSinkConfig pixelsSinkConfig, Properties kafkaProperties) { + 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(); - this.executorService = Executors.newCachedThreadPool(); } private static Set filterTopics(Set topics, String prefix) { @@ -76,7 +77,7 @@ public void run() { } @Override - public void stopMonitor() { + public void stopProcessor() { log.info("Initiating topic monitor shutdown..."); running.set(false); interruptMonitoring(); @@ -86,9 +87,10 @@ public void stopMonitor() { private void shutdownConsumerTasks() { log.info("Shutting down {} active consumer tasks", activeTasks.size()); - activeTasks.forEach((topic, task) -> { + activeTasks.forEach((topic, task) -> + { log.info("Stopping consumer for topic: {}", topic); - task.shutdown(); + task.close(); }); activeTasks.clear(); } @@ -108,7 +110,7 @@ private void initializeResources() { Properties props = new Properties(); props.put(AdminClientConfig.BOOTSTRAP_SERVERS_CONFIG, this.bootstrapServers); this.adminClient = AdminClient.create(props); - this.timer = new Timer("TopicMonitor-Timer", true); + this.timer = new Timer("TopicProcessor-Timer", true); log.info("Started topic monitor for base topic: {}", baseTopic); } @@ -137,7 +139,6 @@ private void interruptMonitoring() { adminClient.close(Duration.ofSeconds(5)); } shutdownExecutorService(); - this.interrupt(); } private void shutdownExecutorService() { @@ -175,7 +176,7 @@ private String extractTableName(String topic) { private void launchConsumerTask(String topic) { try { - TableConsumerTask task = new TableConsumerTask(kafkaProperties, topic); + TableEventKafkaProvider task = new TableEventKafkaProvider(kafkaProperties, topic); executorService.submit(task); } catch (Exception e) { log.error("Failed to start consumer for topic {}: {}", topic, e.getMessage()); @@ -214,9 +215,10 @@ private void processTopicChanges() { private void handleNewTopics(Set newTopics) { newTopics.stream() .filter(this::shouldProcessTable) - .forEach(topic -> { + .forEach(topic -> + { try { - TableConsumerTask task = new TableConsumerTask(kafkaProperties, topic); + TableEventKafkaProvider task = new TableEventKafkaProvider(kafkaProperties, topic); executorService.submit(task); activeTasks.put(topic, task); subscribedTopics.add(topic); diff --git a/src/main/java/io/pixelsdb/pixels/sink/processor/TransactionProcessor.java b/src/main/java/io/pixelsdb/pixels/sink/processor/TransactionProcessor.java new file mode 100644 index 0000000..a76530c --- /dev/null +++ b/src/main/java/io/pixelsdb/pixels/sink/processor/TransactionProcessor.java @@ -0,0 +1,62 @@ +/* + * 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.SinkProto; +import io.pixelsdb.pixels.sink.provider.TransactionEventProvider; +import io.pixelsdb.pixels.sink.writer.PixelsSinkWriter; +import io.pixelsdb.pixels.sink.writer.PixelsSinkWriterFactory; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.concurrent.atomic.AtomicBoolean; + +public class TransactionProcessor implements Runnable, StoppableProcessor { + private static final Logger LOGGER = LoggerFactory.getLogger(TransactionProcessor.class); + private final PixelsSinkWriter sinkWriter; + private final AtomicBoolean running = new AtomicBoolean(true); + private final TransactionEventProvider transactionEventProvider; + + public TransactionProcessor(TransactionEventProvider transactionEventProvider) { + this.transactionEventProvider = transactionEventProvider; + this.sinkWriter = PixelsSinkWriterFactory.getWriter(); + } + + @Override + public void run() { + while (running.get()) { + SinkProto.TransactionMetadata transaction = transactionEventProvider.getTransaction(); + if (transaction == null) { + LOGGER.warn("Received null transaction"); + running.set(false); + break; + } + sinkWriter.writeTrans(transaction); + } + LOGGER.info("Processor thread exited for transaction"); + } + + @Override + public void stopProcessor() { + LOGGER.info("Stopping transaction monitor"); + 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 new file mode 100644 index 0000000..1dd4a6a --- /dev/null +++ b/src/main/java/io/pixelsdb/pixels/sink/provider/EventProvider.java @@ -0,0 +1,155 @@ +/* + * 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 new file mode 100644 index 0000000..a854ebe --- /dev/null +++ b/src/main/java/io/pixelsdb/pixels/sink/provider/ProtoType.java @@ -0,0 +1,52 @@ +/* + * 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; + + +/** + * @package: io.pixelsdb.pixels.sink.provider + * @className: ProtoType + * @author: AntiO2 + * @date: 2025/10/5 12:56 + */ +public enum ProtoType { + ROW(0), + TRANS(1); + + private final int value; + + ProtoType(int value) { + this.value = value; + } + + public static ProtoType fromInt(int value) { + for (ProtoType type : ProtoType.values()) { + if (type.value == value) { + return type; + } + } + throw new IllegalArgumentException("Unknown ProtoType value: " + value); + } + + public int toInt() { + return value; + } +} diff --git a/src/main/java/io/pixelsdb/pixels/sink/provider/TableEventEngineProvider.java b/src/main/java/io/pixelsdb/pixels/sink/provider/TableEventEngineProvider.java new file mode 100644 index 0000000..5f5d110 --- /dev/null +++ b/src/main/java/io/pixelsdb/pixels/sink/provider/TableEventEngineProvider.java @@ -0,0 +1,51 @@ +/* + * 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/consumer/TableConsumerTask.java b/src/main/java/io/pixelsdb/pixels/sink/provider/TableEventKafkaProvider.java similarity index 50% rename from src/main/java/io/pixelsdb/pixels/sink/consumer/TableConsumerTask.java rename to src/main/java/io/pixelsdb/pixels/sink/provider/TableEventKafkaProvider.java index 8e4164c..4b32bb0 100644 --- a/src/main/java/io/pixelsdb/pixels/sink/consumer/TableConsumerTask.java +++ b/src/main/java/io/pixelsdb/pixels/sink/provider/TableEventKafkaProvider.java @@ -1,32 +1,32 @@ /* * 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 + * This file is part of Pixels. * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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. * - * 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. + * 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.consumer; +package io.pixelsdb.pixels.sink.provider; -import io.pixelsdb.pixels.sink.concurrent.TransactionCoordinator; -import io.pixelsdb.pixels.sink.concurrent.TransactionCoordinatorFactory; import io.pixelsdb.pixels.sink.config.PixelsSinkConfig; -import io.pixelsdb.pixels.sink.config.PixelsSinkDefaultConfig; 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.TopicPartition; import org.apache.kafka.common.errors.InterruptException; import org.apache.kafka.common.errors.WakeupException; import org.slf4j.Logger; @@ -36,61 +36,54 @@ import java.time.Duration; import java.util.Collections; import java.util.Properties; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; -import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.atomic.AtomicBoolean; -public class TableConsumerTask implements Runnable { - private static final Logger log = LoggerFactory.getLogger(TableConsumerTask.class); - private static final TransactionCoordinator transactionCoordinator = TransactionCoordinatorFactory.getCoordinator(); +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; - private final ExecutorService executor = Executors.newCachedThreadPool(); - public TableConsumerTask(Properties kafkaProperties, String topic) throws IOException { + + 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 = extractTableName(topic); + this.tableName = DataTransform.extractTableName(topic); } @Override - public void run() { + protected void processLoop() { try { consumer = new KafkaConsumer<>(kafkaProperties); consumer.subscribe(Collections.singleton(topic)); -// TopicPartition partition = new TopicPartition(topic, 0); -// consumer.poll(Duration.ofSeconds(1)); -// consumer.seek(partition, 0); - while (running.get()) { try { ConsumerRecords records = consumer.poll(Duration.ofSeconds(5)); if (!records.isEmpty()) { - log.debug("{} Consumer poll returned {} records", tableName, records.count()); - records.forEach(record -> { - executor.execute(() -> { - RowChangeEvent event = record.value(); - transactionCoordinator.processRowEvent(event); - }); + 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) { - // shutdown normally log.info("Consumer wakeup triggered for {}", tableName); } catch (Exception e) { - e.printStackTrace(); log.info("Exception: {}", e.getMessage()); } finally { if (consumer != null) { @@ -100,16 +93,8 @@ public void run() { } } - public void shutdown() { - running.set(false); - log.info("Shutting down consumer for table: {}", tableName); - if (consumer != null) { - consumer.wakeup(); - } - } - - private String extractTableName(String topic) { - String[] parts = topic.split("\\."); - return parts[parts.length - 1]; + @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 new file mode 100644 index 0000000..2b53699 --- /dev/null +++ b/src/main/java/io/pixelsdb/pixels/sink/provider/TableEventProvider.java @@ -0,0 +1,48 @@ +/* + * 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 new file mode 100644 index 0000000..1676e20 --- /dev/null +++ b/src/main/java/io/pixelsdb/pixels/sink/provider/TableEventStorageLoopProvider.java @@ -0,0 +1,76 @@ +/* + * 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(); + 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); + } + + 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 new file mode 100644 index 0000000..17a37b3 --- /dev/null +++ b/src/main/java/io/pixelsdb/pixels/sink/provider/TableEventStorageProvider.java @@ -0,0 +1,57 @@ +/* + * 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 new file mode 100644 index 0000000..8983c21 --- /dev/null +++ b/src/main/java/io/pixelsdb/pixels/sink/provider/TableProviderAndProcessorPipelineManager.java @@ -0,0 +1,86 @@ +/* + * 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 new file mode 100644 index 0000000..a0f2ecd --- /dev/null +++ b/src/main/java/io/pixelsdb/pixels/sink/provider/TransactionEventEngineProvider.java @@ -0,0 +1,49 @@ +/* + * 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 new file mode 100644 index 0000000..24cf341 --- /dev/null +++ b/src/main/java/io/pixelsdb/pixels/sink/provider/TransactionEventKafkaProvider.java @@ -0,0 +1,87 @@ +/* + * 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 new file mode 100644 index 0000000..7297254 --- /dev/null +++ b/src/main/java/io/pixelsdb/pixels/sink/provider/TransactionEventProvider.java @@ -0,0 +1,37 @@ +/* + * 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 new file mode 100644 index 0000000..9549b3c --- /dev/null +++ b/src/main/java/io/pixelsdb/pixels/sink/provider/TransactionEventStorageLoopProvider.java @@ -0,0 +1,43 @@ +/* + * 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 new file mode 100644 index 0000000..6a1b9e2 --- /dev/null +++ b/src/main/java/io/pixelsdb/pixels/sink/provider/TransactionEventStorageProvider.java @@ -0,0 +1,39 @@ +/* + * 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/sink/PixelsSinkMode.java b/src/main/java/io/pixelsdb/pixels/sink/sink/PixelsSinkMode.java deleted file mode 100644 index d75e36f..0000000 --- a/src/main/java/io/pixelsdb/pixels/sink/sink/PixelsSinkMode.java +++ /dev/null @@ -1,33 +0,0 @@ -/* - * 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.sink; - - -public enum PixelsSinkMode { - CSV, - RETINA; - - public static PixelsSinkMode fromValue(String value) { - for (PixelsSinkMode mode : values()) { - if (mode.name().equalsIgnoreCase(value)) { - return mode; - } - } - throw new RuntimeException(String.format("Can't convert %s to sink type", value)); - } -} diff --git a/src/main/java/io/pixelsdb/pixels/sink/sink/PixelsSinkWriter.java b/src/main/java/io/pixelsdb/pixels/sink/sink/PixelsSinkWriter.java deleted file mode 100644 index a71a345..0000000 --- a/src/main/java/io/pixelsdb/pixels/sink/sink/PixelsSinkWriter.java +++ /dev/null @@ -1,30 +0,0 @@ -/* - * 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.sink; - -import io.pixelsdb.pixels.sink.event.RowChangeEvent; - -import java.io.Closeable; - -public interface PixelsSinkWriter extends Closeable { - void flush(); - - boolean write(RowChangeEvent rowChangeEvent); - - // boolean write(RowChangeEvent rowChangeEvent, ByteBuffer byteBuffer); -} diff --git a/src/main/java/io/pixelsdb/pixels/sink/sink/PixelsSinkWriterFactory.java b/src/main/java/io/pixelsdb/pixels/sink/sink/PixelsSinkWriterFactory.java deleted file mode 100644 index e2f2d05..0000000 --- a/src/main/java/io/pixelsdb/pixels/sink/sink/PixelsSinkWriterFactory.java +++ /dev/null @@ -1,37 +0,0 @@ -/* - * 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.sink; - -import io.pixelsdb.pixels.sink.config.PixelsSinkConfig; -import io.pixelsdb.pixels.sink.config.factory.PixelsSinkConfigFactory; - -import java.io.IOException; - -public class PixelsSinkWriterFactory { - private static final PixelsSinkConfig config = PixelsSinkConfigFactory.getInstance(); - - static public PixelsSinkWriter getWriter() throws IOException { - switch (config.getPixelsSinkMode()) { - case CSV: - return new CsvWriter(); - case RETINA: - return new RetinaWriter(); - } - return null; - } -} diff --git a/src/main/java/io/pixelsdb/pixels/sink/sink/RetinaWriter.java b/src/main/java/io/pixelsdb/pixels/sink/sink/RetinaWriter.java deleted file mode 100644 index 5e1b9a1..0000000 --- a/src/main/java/io/pixelsdb/pixels/sink/sink/RetinaWriter.java +++ /dev/null @@ -1,191 +0,0 @@ -/* - * 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.sink; - -import io.grpc.ManagedChannel; -import io.grpc.ManagedChannelBuilder; -import io.grpc.StatusRuntimeException; -import io.pixelsdb.pixels.common.index.IndexService; -import io.pixelsdb.pixels.index.IndexProto; -import io.pixelsdb.pixels.retina.RetinaProto; -import io.pixelsdb.pixels.retina.RetinaWorkerServiceGrpc; -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.monitor.MetricsFacade; -import io.pixelsdb.pixels.sink.util.LatencySimulator; -import io.prometheus.client.Summary; -import lombok.Getter; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.io.IOException; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicBoolean; - -public class RetinaWriter implements PixelsSinkWriter { - private static final Logger LOGGER = LoggerFactory.getLogger(RetinaWriter.class); - @Getter - private static final PixelsSinkMode pixelsSinkMode = PixelsSinkMode.RETINA; - private static final IndexService indexService = IndexService.Instance(); - - - private static final PixelsSinkConfig config = PixelsSinkConfigFactory.getInstance(); - private final AtomicBoolean isClosed = new AtomicBoolean(false); - // TODO(LiZinuo): 使用RetinaService替换 - final ManagedChannel channel; - final RetinaWorkerServiceGrpc.RetinaWorkerServiceBlockingStub blockingStub; - - private final MetricsFacade metricsFacade = MetricsFacade.getInstance(); - - RetinaWriter() { - if (config.isRpcEnable()) { - // TODO(LiZinuo): 使用RetinaService替换 - this.channel = ManagedChannelBuilder.forAddress( - config.getSinkRemoteHost(), - config.getRemotePort() - ) - .usePlaintext() - .build(); - this.blockingStub = RetinaWorkerServiceGrpc.newBlockingStub(channel); - } else { - channel = null; - blockingStub = null; - } - } - - @Override - public void flush() { - } - - @Override - public boolean write(RowChangeEvent event) { - if (isClosed.get()) { - LOGGER.warn("Attempted to write to closed writer"); - return false; - } - - try { - if (config.isRpcEnable()) { - switch (event.getOp()) { - case INSERT: - case SNAPSHOT: - return sendInsertRequest(event); - case UPDATE: - return sendUpdateRequest(event); - case DELETE: - return sendDeleteRequest(event); - case UNRECOGNIZED: - break; - } - } else { - if (event.getOp() != SinkProto.OperationType.INSERT && event.getOp() != SinkProto.OperationType.SNAPSHOT) { - Summary.Timer indexLatencyTimer = metricsFacade.startIndexLatencyTimer(); - LatencySimulator.smartDelay(); // Mock Look Up Index - indexLatencyTimer.close(); - } - Summary.Timer retinaLatencyTimer = metricsFacade.startRetinaLatencyTimer(); - LatencySimulator.smartDelay(); // Call Retina - retinaLatencyTimer.close(); - return true; - } - - } catch (StatusRuntimeException e) { - LOGGER.error("gRPC write failed for event {}: {}", event.getTransaction().getId(), e.getStatus()); - return false; - } finally { - - } - // TODO: error handle - return false; - } - - private boolean sendInsertRequest(RowChangeEvent event) { - // TODO 这里需要RetinaService提供包装接口 - RetinaProto.InsertRecordResponse insertRecordResponse = blockingStub.insertRecord(getInsertRecordRequest(event)); - return insertRecordResponse.getHeader().getErrorCode() == 0; - } - - private RetinaProto.InsertRecordRequest getInsertRecordRequest(RowChangeEvent event) { - // Step1. Serialize Row Data - RetinaProto.InsertRecordRequest.Builder builder = RetinaProto.InsertRecordRequest.newBuilder(); - RetinaProto.RowValue.Builder rowValueBuilder = builder.getRowBuilder(); - event.getAfterData().getValuesList().forEach(value -> - builder.setRow(rowValueBuilder.addValues(value.getValue()))); - - // Step2. Build Insert Request - return builder - .setSchema(event.getDb()) - .setTable(event.getTable()) - .setRow(rowValueBuilder.build()) - .setTimestamp(event.getTimeStamp()) - .setTransInfo(getTransinfo(event)) - .build(); - } - - private boolean sendDeleteRequest(RowChangeEvent event) { - RetinaProto.DeleteRecordResponse deleteRecordResponse = blockingStub.deleteRecord(getDeleteRecordRequest(event)); - return deleteRecordResponse.getHeader().getErrorCode() == 0; - } - - @Override - public void close() throws IOException { - if (isClosed.compareAndSet(false, true)) { - try { - channel.shutdown(); - if (!channel.awaitTermination(5, TimeUnit.SECONDS)) { - channel.shutdownNow(); - } - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - throw new IOException("Channel shutdown interrupted", e); - } - } - } - - private RetinaProto.DeleteRecordRequest getDeleteRecordRequest(RowChangeEvent event) { - // Step1. Look up unique index to find row location - IndexProto.RowLocation rowLocation = indexService.lookupUniqueIndex(event.getIndexKey()); - - // Step2. Build Delete Request - RetinaProto.DeleteRecordRequest.Builder builder = RetinaProto.DeleteRecordRequest.newBuilder(); - return builder - .setRow(builder.getRowBuilder().setRgRowId(rowLocation.getRgRowId()).setFileId(rowLocation.getFileId()).setRgId(rowLocation.getRgId())) - .setTimestamp(event.getTimeStamp()) - .setTransInfo(getTransinfo(event)) - .build(); - } - - private boolean sendUpdateRequest(RowChangeEvent event) { - // Delete & Insert - RetinaProto.DeleteRecordResponse deleteRecordResponse = blockingStub.deleteRecord(getDeleteRecordRequest(event)); - if (deleteRecordResponse.getHeader().getErrorCode() != 0) { - return false; - } - - RetinaProto.InsertRecordResponse insertRecordResponse = blockingStub.insertRecord(getInsertRecordRequest(event)); - return insertRecordResponse.getHeader().getErrorCode() == 0; - } - - private RetinaProto.TransInfo getTransinfo(RowChangeEvent event) { - return RetinaProto.TransInfo.newBuilder() - .setOrder(event.getTransaction().getTotalOrder()) - .setTransId(event.getTransaction().getId().hashCode()).build(); - } -} diff --git a/src/main/java/io/pixelsdb/pixels/sink/source/SinkSource.java b/src/main/java/io/pixelsdb/pixels/sink/source/SinkSource.java new file mode 100644 index 0000000..a96dc56 --- /dev/null +++ b/src/main/java/io/pixelsdb/pixels/sink/source/SinkSource.java @@ -0,0 +1,37 @@ +/* + * 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; + + +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 { + void start(); + + boolean isRunning(); +} diff --git a/src/main/java/io/pixelsdb/pixels/sink/source/SinkSourceFactory.java b/src/main/java/io/pixelsdb/pixels/sink/source/SinkSourceFactory.java new file mode 100644 index 0000000..02b293b --- /dev/null +++ b/src/main/java/io/pixelsdb/pixels/sink/source/SinkSourceFactory.java @@ -0,0 +1,40 @@ +/* + * 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; + +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; + +public class SinkSourceFactory { + public static SinkSource createSinkSource() { + PixelsSinkConfig config = PixelsSinkConfigFactory.getInstance(); + return switch (config.getDataSource()) { + case "kafka" -> new SinkKafkaSource(); + case "engine" -> new SinkEngineSource(); + case "storage" -> new FasterSinkStorageSource(); + default -> throw new IllegalStateException("Unsupported data source type: " + config.getDataSource()); + }; + } +} 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 new file mode 100644 index 0000000..715cab6 --- /dev/null +++ b/src/main/java/io/pixelsdb/pixels/sink/source/engine/PixelsDebeziumConsumer.java @@ -0,0 +1,111 @@ +/* + * 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.engine; + + +import io.debezium.engine.DebeziumEngine; +import io.debezium.engine.RecordChangeEvent; +import io.pixelsdb.pixels.common.metadata.SchemaTableName; +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.util.MetricsFacade; +import org.apache.kafka.connect.data.Struct; +import org.apache.kafka.connect.source.SourceRecord; + +import java.util.List; + +/** + * @package: io.pixelsdb.pixels.source + * @className: PixelsDebeziumConsumer + * @author: AntiO2 + * @date: 2025/9/25 12:51 + */ +public class PixelsDebeziumConsumer implements DebeziumEngine.ChangeConsumer>, StoppableProcessor { + 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 MetricsFacade metricsFacade = MetricsFacade.getInstance(); + PixelsSinkConfig pixelsSinkConfig = PixelsSinkConfigFactory.getInstance(); + + public PixelsDebeziumConsumer() { + this.checkTransactionTopic = pixelsSinkConfig.getDebeziumTopicPrefix() + ".transaction"; + this.transactionProviderThread = new Thread(this.transactionEventProvider, "transaction-adapter"); + this.transactionProcessorThread = new Thread(this.processor, "transaction-processor"); + + this.transactionProcessorThread.start(); + this.transactionProviderThread.start(); + } + + + public void handleBatch(List> event, + DebeziumEngine.RecordCommitter> committer) throws InterruptedException { + for (RecordChangeEvent record : event) { + try { + SourceRecord sourceRecord = record.record(); + if (sourceRecord == null) { + continue; + } + + metricsFacade.recordDebeziumEvent(); + if (isTransactionEvent(sourceRecord)) { + handleTransactionSourceRecord(sourceRecord); + } else { + handleRowChangeSourceRecord(sourceRecord); + } + } finally { + committer.markProcessed(record); + } + + } + committer.markBatchFinished(); + } + + private void handleTransactionSourceRecord(SourceRecord sourceRecord) throws InterruptedException { + transactionEventProvider.putTransRawEvent(sourceRecord); + } + + private void handleRowChangeSourceRecord(SourceRecord sourceRecord) { + Struct value = (Struct) sourceRecord.value(); + Struct source = (Struct) value.get("source"); + String schemaName = source.get("db").toString(); + String tableName = source.get("table").toString(); + SchemaTableName schemaTableName = new SchemaTableName(schemaName, tableName); + tableProvidersManagerImpl.routeRecord(schemaTableName, sourceRecord); + } + + private boolean isTransactionEvent(SourceRecord sourceRecord) { + return checkTransactionTopic.equals(sourceRecord.topic()); + } + + @Override + public void stopProcessor() { + transactionProviderThread.interrupt(); + processor.stopProcessor(); + transactionProcessorThread.interrupt(); + } +} 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 new file mode 100644 index 0000000..c573e40 --- /dev/null +++ b/src/main/java/io/pixelsdb/pixels/sink/source/engine/SinkEngineSource.java @@ -0,0 +1,79 @@ +/* + * 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.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.factory.PixelsSinkConfigFactory; +import io.pixelsdb.pixels.sink.source.SinkSource; +import org.apache.kafka.connect.source.SourceRecord; + +import java.util.Properties; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +public class SinkEngineSource implements SinkSource { + private final PixelsDebeziumConsumer consumer; + private DebeziumEngine> engine; + private ExecutorService executor; + private volatile boolean running = true; + + public SinkEngineSource() { + this.consumer = new PixelsDebeziumConsumer(); + } + + public void start() { + Properties debeziumProps = PixelsSinkConfigFactory.getInstance() + .getConfig().extractPropertiesByPrefix("debezium.", true); + + this.engine = DebeziumEngine.create(ChangeEventFormat.of(Connect.class)) + .using(debeziumProps) + .notifying(consumer) + .build(); + + this.executor = Executors.newSingleThreadExecutor(); + this.executor.execute(engine); + } + + @Override + public void stopProcessor() { + try { + if (engine != null) { + engine.close(); + } + if (executor != null) { + executor.shutdown(); + } + consumer.stopProcessor(); + } catch (Exception e) { + throw new RuntimeException("Failed to stop PixelsSinkEngine", e); + } finally { + running = false; + } + } + + @Override + public boolean isRunning() { + return running; + } +} 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 new file mode 100644 index 0000000..0dba2fb --- /dev/null +++ b/src/main/java/io/pixelsdb/pixels/sink/source/kafka/SinkKafkaSource.java @@ -0,0 +1,69 @@ +/* + * 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.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.source.SinkSource; + +import java.util.Properties; + +public class SinkKafkaSource implements SinkSource { + private MonitorThreadManager manager; + private volatile boolean running = true; + + @Override + public void start() { + PixelsSinkConfig pixelsSinkConfig = PixelsSinkConfigFactory.getInstance(); + KafkaPropFactorySelector kafkaPropFactorySelector = new KafkaPropFactorySelector(); + + Properties transactionKafkaProperties = kafkaPropFactorySelector + .getFactory(PixelsSinkConstants.TRANSACTION_KAFKA_PROP_FACTORY) + .createKafkaProperties(pixelsSinkConfig); + TransactionProcessor transactionProcessor = null; // TODO: new TransactionProcessor(); + + Properties topicKafkaProperties = kafkaPropFactorySelector + .getFactory(PixelsSinkConstants.ROW_RECORD_KAFKA_PROP_FACTORY) + .createKafkaProperties(pixelsSinkConfig); + TopicProcessor topicMonitor = new TopicProcessor(pixelsSinkConfig, topicKafkaProperties); + + manager = new MonitorThreadManager(); + manager.startMonitor(transactionProcessor); + manager.startMonitor(topicMonitor); + } + + + @Override + public void stopProcessor() { + manager.shutdown(); + running = false; + } + + @Override + public boolean isRunning() { + return running; + } +} 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 new file mode 100644 index 0000000..20b3009 --- /dev/null +++ b/src/main/java/io/pixelsdb/pixels/sink/source/storage/AbstractMemorySinkStorageSource.java @@ -0,0 +1,132 @@ +/* + * 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 + preloadedRecords.add(new Pair<>(key, valueBuffer)); + } + } + + 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 buffer = record.getRight(); + + // 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(buffer); + + 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 new file mode 100644 index 0000000..08d7a0d --- /dev/null +++ b/src/main/java/io/pixelsdb/pixels/sink/source/storage/AbstractReaderSinkStorageSource.java @@ -0,0 +1,111 @@ +/* + * 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 new file mode 100644 index 0000000..0e4cd9d --- /dev/null +++ b/src/main/java/io/pixelsdb/pixels/sink/source/storage/AbstractSinkStorageSource.java @@ -0,0 +1,170 @@ +/* + * 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.core.utils.Pair; +import io.pixelsdb.pixels.sink.config.PixelsSinkConfig; +import io.pixelsdb.pixels.sink.config.factory.PixelsSinkConfigFactory; +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.TransactionEventStorageLoopProvider; +import io.pixelsdb.pixels.sink.source.SinkSource; +import io.pixelsdb.pixels.sink.util.EtcdFileRegistry; +import io.pixelsdb.pixels.sink.util.FlushRateLimiter; +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.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.ExecutionException; +import java.util.concurrent.atomic.AtomicBoolean; + +public abstract class AbstractSinkStorageSource implements SinkSource { + private static final Logger LOGGER = LoggerFactory.getLogger(AbstractSinkStorageSource.class); + protected final AtomicBoolean running = new AtomicBoolean(false); + + protected final String topic; + protected final String baseDir; + protected final EtcdFileRegistry etcdFileRegistry; + protected final List files; + protected final CompletableFuture POISON_PILL = new CompletableFuture<>(); + protected final Map consumerThreads = new ConcurrentHashMap<>(); + protected final Map, Integer>>> queueMap = new ConcurrentHashMap<>(); + protected final boolean storageLoopEnabled; + protected final FlushRateLimiter sourceRateLimiter; + private final TableMetadataRegistry tableMetadataRegistry = TableMetadataRegistry.Instance(); + private final MetricsFacade metricsFacade = MetricsFacade.getInstance(); + private final TableProviderAndProcessorPipelineManager> tablePipelineManager = new TableProviderAndProcessorPipelineManager<>(); + protected TransactionEventStorageLoopProvider> transactionEventProvider; + protected TransactionProcessor transactionProcessor; + protected Thread transactionProviderThread; + protected Thread transactionProcessorThread; + protected int loopId = 0; + protected List readers = new ArrayList<>(); + + protected AbstractSinkStorageSource() { + PixelsSinkConfig pixelsSinkConfig = PixelsSinkConfigFactory.getInstance(); + this.topic = pixelsSinkConfig.getSinkProtoData(); + this.baseDir = pixelsSinkConfig.getSinkProtoDir(); + this.etcdFileRegistry = new EtcdFileRegistry(topic, baseDir); + this.files = this.etcdFileRegistry.listAllFiles(); + this.storageLoopEnabled = pixelsSinkConfig.isSinkStorageLoop(); + + this.transactionEventProvider = new TransactionEventStorageLoopProvider<>(); + this.transactionProviderThread = new Thread(transactionEventProvider); + + this.transactionProcessor = new TransactionProcessor(transactionEventProvider); + this.transactionProcessorThread = new Thread(transactionProcessor, "debezium-processor"); + this.sourceRateLimiter = FlushRateLimiter.getNewInstance(); + } + + abstract ProtoType getProtoType(int i); + + protected void clean() { + queueMap.values().forEach(q -> + { + try { + q.put(new Pair<>(POISON_PILL, loopId)); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + }); + + consumerThreads.values().forEach(t -> + { + try { + t.join(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + }); + + for (PhysicalReader reader : readers) { + try { + reader.close(); + } catch (IOException e) { + LOGGER.warn("Failed to close reader", e); + } + } + } + + protected void handleTransactionSourceRecord(ByteBuffer record, Integer loopId) { + transactionEventProvider.putTransRawEvent(new Pair<>(record, loopId)); + } + + protected void consumeQueue(int key, BlockingQueue, Integer>> queue, ProtoType protoType) { + try { + while (true) { + Pair, Integer> pair = queue.take(); + CompletableFuture value = pair.getLeft(); + int loopId = pair.getRight(); + if (value == POISON_PILL) { + break; + } + ByteBuffer valueBuffer = value.get(); + metricsFacade.recordDebeziumEvent(); + switch (protoType) { + case ROW -> handleRowChangeSourceRecord(key, valueBuffer, loopId); + case TRANS -> handleTransactionSourceRecord(valueBuffer, loopId); + } + } + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } catch (ExecutionException e) { + LOGGER.error("Error in async processing", e); + } + } + + protected ByteBuffer copyToHeap(ByteBuffer directBuffer) { + ByteBuffer duplicate = directBuffer.duplicate(); + ByteBuffer heapBuffer = ByteBuffer.allocate(duplicate.remaining()); + heapBuffer.put(duplicate); + heapBuffer.flip(); + return heapBuffer; + } + + protected void handleRowChangeSourceRecord(int key, ByteBuffer dataBuffer, int loopId) { + tablePipelineManager.routeRecord(key, new Pair<>(dataBuffer, loopId)); + } + + @Override + public boolean isRunning() { + return running.get(); + } + + @Override + public void stopProcessor() { + running.set(false); + transactionProviderThread.interrupt(); + transactionProcessorThread.interrupt(); + transactionProcessor.stopProcessor(); + } +} 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 new file mode 100644 index 0000000..6f98c46 --- /dev/null +++ b/src/main/java/io/pixelsdb/pixels/sink/source/storage/FasterSinkStorageSource.java @@ -0,0 +1,60 @@ +/* + * 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 new file mode 100644 index 0000000..567700f --- /dev/null +++ b/src/main/java/io/pixelsdb/pixels/sink/source/storage/LegacySinkStorageSource.java @@ -0,0 +1,217 @@ +/* + * 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/util/BlockingBoundedMap.java b/src/main/java/io/pixelsdb/pixels/sink/util/BlockingBoundedMap.java new file mode 100644 index 0000000..a48348c --- /dev/null +++ b/src/main/java/io/pixelsdb/pixels/sink/util/BlockingBoundedMap.java @@ -0,0 +1,119 @@ +/* + * 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.util; + +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.Semaphore; +import java.util.function.BiFunction; + +/** + * A thread-safe bounded map that blocks when full. + *

+ * Similar to ConcurrentHashMap, but with a capacity limit. + * When the map reaches its maximum size, any new insertion or compute + * for a new key will block until space becomes available. + */ +public class BlockingBoundedMap { + private final int maxSize; + private final Semaphore semaphore; + private final ConcurrentMap map; + + public BlockingBoundedMap(int maxSize) { + this.maxSize = maxSize; + this.map = new ConcurrentHashMap<>(); + this.semaphore = new Semaphore(maxSize); + } + + /** + * Puts a key-value pair into the map. + * If the map is full, this call blocks until space becomes available. + */ + private void put(K key, V value) throws InterruptedException { + semaphore.acquire(); // block if full + V prev = map.put(key, value); + if (prev != null) { + // replaced existing value — no new space consumed + semaphore.release(); + } + } + + public V get(K key) { + return map.get(key); + } + + /** + * Removes a key from the map and releases one permit if a value was present. + */ + public V remove(K key) { + V val = map.remove(key); + if (val != null) { + semaphore.release(); + } + return val; + } + + public int size() { + return map.size(); + } + + /** + * Atomically computes a new value for a key, blocking if capacity is full. + *

+ * - If the key is new and capacity is full, this method blocks until space is freed. + * - If the key already exists, it does not block. + * - If the remapping function returns null, the key is removed and capacity is released. + */ + public V compute(K key, BiFunction remappingFunction) { + for (; ; ) { + V oldVal = map.get(key); + if (oldVal == null) { + try { + semaphore.acquire(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + return null; + } + + V newVal = remappingFunction.apply(key, null); + if (newVal == null) { + semaphore.release(); + return null; + } + + V existing = map.putIfAbsent(key, newVal); + if (existing == null) { + return newVal; + } else { + semaphore.release(); + continue; + } + } else { + return map.compute(key, remappingFunction); + } + } + } + + public Set keySet() { + return map.keySet(); + } +} diff --git a/src/main/java/io/pixelsdb/pixels/sink/util/DataTransform.java b/src/main/java/io/pixelsdb/pixels/sink/util/DataTransform.java new file mode 100644 index 0000000..fd3c872 --- /dev/null +++ b/src/main/java/io/pixelsdb/pixels/sink/util/DataTransform.java @@ -0,0 +1,138 @@ +/* + * 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.util; + +import com.google.protobuf.ByteString; +import io.pixelsdb.pixels.retina.RetinaProto; +import io.pixelsdb.pixels.sink.SinkProto; + +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.List; + +public class DataTransform { + private static ByteString longToByteString(long value) { + byte[] bytes = ByteBuffer.allocate(Long.BYTES).putLong(value).array(); + return ByteString.copyFrom(bytes); + } + + @Deprecated + public static void updateTimeStamp(List updateData, long txStartTime) { + ByteString timestampBytes = longToByteString(txStartTime); + + for (RetinaProto.TableUpdateData.Builder tableUpdateDataBuilder : updateData) { + int insertDataCount = tableUpdateDataBuilder.getInsertDataCount(); + for (int i = 0; i < insertDataCount; i++) { + RetinaProto.InsertData.Builder insertBuilder = tableUpdateDataBuilder.getInsertDataBuilder(i); + int colValueCount = insertBuilder.getColValuesCount(); + if (colValueCount > 0) { + insertBuilder.setColValues(colValueCount - 1, timestampBytes); + } + } + + int updateDataCount = tableUpdateDataBuilder.getUpdateDataCount(); + for (int i = 0; i < updateDataCount; i++) { + RetinaProto.UpdateData.Builder updateBuilder = tableUpdateDataBuilder.getUpdateDataBuilder(i); + + int colValueCount = updateBuilder.getColValuesCount(); + if (colValueCount > 0) { + updateBuilder.setColValues(colValueCount - 1, timestampBytes); + } + } + } + } + + + public static List updateRecordTimestamp(List records, long timestamp) { + if (records == null || records.isEmpty()) { + return records; + } + SinkProto.ColumnValue timestampColumn = getTimestampColumn(timestamp); + List updatedRecords = new ArrayList<>(records.size()); + for (SinkProto.RowRecord record : records) { + + updatedRecords.add(updateRecordTimestamp(record, timestampColumn)); + } + return updatedRecords; + } + + private static SinkProto.ColumnValue getTimestampColumn(long timestamp) { + ByteString timestampBytes = longToByteString(timestamp); + return SinkProto.ColumnValue.newBuilder().setValue(timestampBytes).build(); + } + + public static SinkProto.RowRecord updateRecordTimestamp(SinkProto.RowRecord record, long timestamp) { + if (record == null) { + return null; + } + SinkProto.ColumnValue timestampColumn = getTimestampColumn(timestamp); + return updateRecordTimestamp(record, timestampColumn); + } + + public static void updateRecordTimestamp(SinkProto.RowRecord.Builder recordBuilder, long timestamp) { + switch (recordBuilder.getOp()) { + case INSERT: + case UPDATE: + case SNAPSHOT: + if (recordBuilder.hasAfter()) { + SinkProto.RowValue.Builder afterBuilder = recordBuilder.getAfterBuilder(); + int colCount = afterBuilder.getValuesCount(); + if (colCount > 0) { + afterBuilder.setValues(colCount - 1, getTimestampColumn(timestamp)); + } + } + break; + case DELETE: + default: + break; + } + } + + private static SinkProto.RowRecord updateRecordTimestamp(SinkProto.RowRecord.Builder recordBuilder, SinkProto.ColumnValue timestampColumn) { + switch (recordBuilder.getOp()) { + case INSERT: + case UPDATE: + case SNAPSHOT: + if (recordBuilder.hasAfter()) { + SinkProto.RowValue.Builder afterBuilder = recordBuilder.getAfterBuilder(); + int colCount = afterBuilder.getValuesCount(); + if (colCount > 0) { + afterBuilder.setValues(colCount - 1, timestampColumn); + } + } + break; + case DELETE: + default: + break; + } + return recordBuilder.build(); + } + + private static SinkProto.RowRecord updateRecordTimestamp(SinkProto.RowRecord record, SinkProto.ColumnValue timestampColumn) { + SinkProto.RowRecord.Builder recordBuilder = record.toBuilder(); + return updateRecordTimestamp(recordBuilder, timestampColumn); + } + + public static String extractTableName(String topic) { + String[] parts = topic.split("\\."); + return parts[parts.length - 1]; + } +} diff --git a/src/main/java/io/pixelsdb/pixels/sink/util/DateUtil.java b/src/main/java/io/pixelsdb/pixels/sink/util/DateUtil.java new file mode 100644 index 0000000..41b2d6f --- /dev/null +++ b/src/main/java/io/pixelsdb/pixels/sink/util/DateUtil.java @@ -0,0 +1,83 @@ +/* + * 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.util; + + +import io.pixelsdb.pixels.core.utils.DatetimeUtils; + +import java.text.DateFormat; +import java.text.SimpleDateFormat; +import java.time.Instant; +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.time.format.DateTimeFormatter; +import java.util.Calendar; +import java.util.Date; + +/** + * @package: io.pixelsdb.pixels.sink.util + * @className: DateUtil + * @author: AntiO2 + * @date: 2025/8/21 17:31 + */ +public class DateUtil { + + public static Date fromDebeziumDate(int epochDay) { + Calendar cal = Calendar.getInstance(); + cal.clear(); + cal.set(1970, Calendar.JANUARY, 1); // epoch 起点 + cal.add(Calendar.DAY_OF_MONTH, epochDay); // 加上天数 + return cal.getTime(); + } + + // TIMESTAMP(1), TIMESTAMP(2), TIMESTAMP(3) + public static Date fromDebeziumTimestamp(long epochTs) { + return new Date(epochTs / 1000); + } + + public static String convertDateToDayString(Date date) { + // "yyyy-MM-dd HH:mm:ss.SSS" + DateFormat df = new SimpleDateFormat("yyyy-MM-dd"); + String dateToString = df.format(date); + return (dateToString); + } + + public static String convertDateToString(Date date) { + // "yyyy-MM-dd HH:mm:ss.SSS" + DateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS"); + String dateToString = df.format(date); + return (dateToString); + } + + public static String convertTimestampToString(Date date) { + if (date == null) { + return null; + } + SimpleDateFormat sdf = new SimpleDateFormat("HH:mm:ss.SSS"); + return sdf.format(date); + } + + public static String convertDebeziumTimestampToString(long epochTs) { + LocalDateTime dateTime = LocalDateTime.ofInstant(Instant.ofEpochMilli(epochTs), ZoneId.systemDefault()); + DateTimeFormatter formatter = DatetimeUtils.SQL_LOCAL_DATE_TIME; + return dateTime.format(formatter); + } +} diff --git a/src/main/java/io/pixelsdb/pixels/sink/util/EtcdFileRegistry.java b/src/main/java/io/pixelsdb/pixels/sink/util/EtcdFileRegistry.java new file mode 100644 index 0000000..999fa3e --- /dev/null +++ b/src/main/java/io/pixelsdb/pixels/sink/util/EtcdFileRegistry.java @@ -0,0 +1,173 @@ +/* + * 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.util; + + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import io.etcd.jetcd.KeyValue; +import io.pixelsdb.pixels.common.utils.EtcdUtil; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.stream.Collectors; + +/** + * @package: io.pixelsdb.pixels.sink.util + * @className: EtcdFileRegistry + * @author: AntiO2 + * @date: 2025/10/5 08:24 + */ +public class EtcdFileRegistry { + private static final Logger LOGGER = LoggerFactory.getLogger(EtcdFileRegistry.class); + + private static final String REGISTRY_PREFIX = "/sink/proto/registry/"; + private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); + private final String topic; + private final String baseDir; + private final EtcdUtil etcd = EtcdUtil.Instance(); + private final AtomicInteger nextFileId = new AtomicInteger(0); + private String currentFileKey; + + public EtcdFileRegistry(String topic, String baseDir) { + this.topic = topic; + this.baseDir = baseDir; + initRegistry(); + } + + public static String extractPath(String etcdValue) { + try { + Map meta = OBJECT_MAPPER.readValue(etcdValue, Map.class); + return (String) meta.get("path"); + } catch (IOException e) { + LOGGER.error("Failed to parse etcd value: {}", etcdValue, e); + return null; + } + } + + private void initRegistry() { + List files = etcd.getKeyValuesByPrefix(filePrefix()); + if (!files.isEmpty()) { + int maxId = files.stream() + .mapToInt(kv -> extractFileId(kv.getKey().toString())) + .max() + .orElse(0); + nextFileId.set(maxId + 1); + LOGGER.info("Initialized registry for topic {} with nextFileId={}", topic, nextFileId.get()); + } else { + LOGGER.info("No existing files found for topic {}, starting fresh", topic); + } + } + + private String topicPrefix() { + return REGISTRY_PREFIX + topic; + } + + private String filePrefix() { + return topicPrefix() + "/files/"; + } + + private int extractFileId(String key) { + try { + String fileName = key.substring(key.lastIndexOf('/') + 1); + String id = fileName.replace(".proto", ""); + return Integer.parseInt(id); + } catch (Exception e) { + return 0; + } + } + + /** + * Create a new file and register it in etcd. + */ + public synchronized String createNewFile() { + String fileName = String.format("%05d.proto", nextFileId.getAndIncrement()); + String fullPath = baseDir + "/" + topic + "/" + fileName; + + Map fileMeta = new HashMap<>(); + fileMeta.put("path", fullPath); + fileMeta.put("created_at", String.valueOf(System.currentTimeMillis())); + fileMeta.put("status", "active"); + currentFileKey = filePrefix() + fileName; + + String jsonValue = null; + try { + jsonValue = OBJECT_MAPPER.writeValueAsString(fileMeta); + } catch (JsonProcessingException e) { + throw new RuntimeException(e); + } + + currentFileKey = filePrefix() + fileName; + etcd.putKeyValue(currentFileKey, jsonValue); + etcd.putKeyValue(topicPrefix() + "/current", fileName); + LOGGER.info("Created new file [{}] for topic [{}]", fileName, topic); + return fullPath; + } + + public synchronized String getCurrentFileKey() { + return currentFileKey; + } + + /** + * List all files (for readers). + */ + public List listAllFiles() { + List files = etcd.getKeyValuesByPrefix(filePrefix()); + return files.stream() + .map(kv -> + { + String value = kv.getValue().toString(); + return extractPath(value); + }) + .sorted() + .collect(Collectors.toList()); + } + + /** + * Mark a file as completed (for writer rotation). + */ + public void markFileCompleted(String fileName) { + KeyValue kv = etcd.getKeyValue(fileName); + if (kv == null) return; + + Map meta = null; + try { + meta = OBJECT_MAPPER.readValue(kv.getValue().toString(), Map.class); + meta.put("completed_at", String.valueOf(System.currentTimeMillis())); + meta.put("status", "completed"); + String jsonValue = OBJECT_MAPPER.writeValueAsString(meta); + etcd.putKeyValue(fileName, jsonValue); + } catch (JsonProcessingException e) { + throw new RuntimeException(e); + } + + LOGGER.info("Marked file [{}] as completed", fileName); + } + + public void cleanData() { + etcd.deleteByPrefix(topicPrefix()); + } +} diff --git a/src/main/java/io/pixelsdb/pixels/sink/util/FlushRateLimiter.java b/src/main/java/io/pixelsdb/pixels/sink/util/FlushRateLimiter.java new file mode 100644 index 0000000..8fa0276 --- /dev/null +++ b/src/main/java/io/pixelsdb/pixels/sink/util/FlushRateLimiter.java @@ -0,0 +1,117 @@ +/* + * 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.util; + +import io.pixelsdb.pixels.sink.config.PixelsSinkConfig; +import io.pixelsdb.pixels.sink.config.factory.PixelsSinkConfigFactory; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.Semaphore; +import java.util.concurrent.TimeUnit; + +public class FlushRateLimiter { + private static final Logger LOGGER = LoggerFactory.getLogger(FlushRateLimiter.class); + // Configuration derived parameters + private static final long REFRESH_PERIOD_MS = 10; + private static volatile FlushRateLimiter instance; + private final Semaphore semaphore; + private final boolean enableRateLimiter; + private final ScheduledExecutorService scheduler; + private final int replenishmentAmount; + + private FlushRateLimiter() { + PixelsSinkConfig pixelsSinkConfig = PixelsSinkConfigFactory.getInstance(); + int sourceRateLimit = pixelsSinkConfig.getSourceRateLimit(); + this.enableRateLimiter = pixelsSinkConfig.isEnableSourceRateLimit(); + + if (sourceRateLimit <= 0 || !enableRateLimiter) { + this.semaphore = null; + this.replenishmentAmount = 0; + this.scheduler = null; + return; + } + + double replenishmentPerMillisecond = (double) sourceRateLimit / 1000.0; + this.replenishmentAmount = (int) Math.max(1, Math.round(replenishmentPerMillisecond * REFRESH_PERIOD_MS)); + + this.semaphore = new Semaphore(this.replenishmentAmount); + + this.scheduler = Executors.newSingleThreadScheduledExecutor(r -> + { + Thread t = Executors.defaultThreadFactory().newThread(r); + t.setName("Rate-Limiter-Replenish"); + t.setDaemon(true); + return t; + }); + + this.scheduler.scheduleAtFixedRate( + this::replenishTokens, + REFRESH_PERIOD_MS, + REFRESH_PERIOD_MS, + TimeUnit.MILLISECONDS + ); + + LOGGER.info("FlushRateLimiter initialized. Rate: {}/s, Replenishment: {} tokens every {}ms.", + sourceRateLimit, this.replenishmentAmount, REFRESH_PERIOD_MS); + } + + public static FlushRateLimiter getInstance() { + if (instance == null) { + synchronized (FlushRateLimiter.class) { + if (instance == null) { + instance = new FlushRateLimiter(); + } + } + } + return instance; + } + + public static FlushRateLimiter getNewInstance() { + return new FlushRateLimiter(); + } + + private void replenishTokens() { + if (semaphore != null) { + semaphore.release(replenishmentAmount); + } + } + + public void acquire(int num) { + if (enableRateLimiter && semaphore != null) { + try { + semaphore.acquire(num); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + LOGGER.warn("FlushRateLimiter acquire interrupted.", e); + } + } + } + + public void shutdown() { + if (scheduler != null) { + scheduler.shutdownNow(); + LOGGER.info("FlushRateLimiter scheduler stopped."); + } + } +} \ No newline at end of file diff --git a/src/main/java/io/pixelsdb/pixels/sink/util/LatencySimulator.java b/src/main/java/io/pixelsdb/pixels/sink/util/LatencySimulator.java index 1a4913d..a89d571 100644 --- a/src/main/java/io/pixelsdb/pixels/sink/util/LatencySimulator.java +++ b/src/main/java/io/pixelsdb/pixels/sink/util/LatencySimulator.java @@ -1,18 +1,21 @@ /* * 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 + * This file is part of Pixels. * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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. * - * 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. + * 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.util; diff --git a/src/main/java/io/pixelsdb/pixels/sink/util/MetricsFacade.java b/src/main/java/io/pixelsdb/pixels/sink/util/MetricsFacade.java new file mode 100644 index 0000000..9001433 --- /dev/null +++ b/src/main/java/io/pixelsdb/pixels/sink/util/MetricsFacade.java @@ -0,0 +1,567 @@ +/* + * 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.util; + +import com.google.protobuf.ByteString; +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.freshness.FreshnessHistory; +import io.pixelsdb.pixels.sink.freshness.OneSecondAverage; +import io.pixelsdb.pixels.sink.writer.retina.SinkContextManager; +import io.prometheus.client.Counter; +import io.prometheus.client.Histogram; +import io.prometheus.client.Summary; +import lombok.Setter; +import org.apache.commons.math3.stat.descriptive.SynchronizedDescriptiveStatistics; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.FileWriter; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.List; +import java.util.concurrent.atomic.AtomicBoolean; + +public class MetricsFacade { + private static final Logger LOGGER = LoggerFactory.getLogger(MetricsFacade.class); + private static final PixelsSinkConfig config = PixelsSinkConfigFactory.getInstance(); + private static MetricsFacade instance; + private final boolean enabled; + private final Counter tableChangeCounter; + private final Counter rowChangeCounter; + private final Counter transactionCounter; + private final Counter serdRowRecordCounter; + private final Counter serdTxRecordCounter; + private final Summary processingLatency; + private final Counter rawDataThroughputCounter; + private final Counter debeziumEventCounter; + private final Counter rowEventCounter; + private final Summary transServiceLatency; + private final Summary indexServiceLatency; + private final Summary retinaServiceLatency; + private final Summary writerLatency; + private final Summary totalLatency; + private final Summary tableFreshness; + private final Histogram transactionRowCountHistogram; + private final Histogram primaryKeyUpdateDistribution; + + private final boolean monitorReportEnabled; + private final int monitorReportInterval; + private final int freshnessReportInterval; + + private final SynchronizedDescriptiveStatistics freshness; + private final SynchronizedDescriptiveStatistics rowChangeSpeed; + private final OneSecondAverage freshnessAvg; + private final Boolean freshnessVerbose; + private final FreshnessHistory freshnessHistory; + + private final String monitorReportPath; + private final String freshnessReportPath; + + private final AtomicBoolean running = new AtomicBoolean(false); + private final Thread reportThread; + private final Thread freshnessThread; + @Setter + private SinkContextManager sinkContextManager; + private long lastRowChangeCount = 0; + private long lastTransactionCount = 0; + private long lastDebeziumCount = 0; + private long lastSerdRowRecordCount = 0; + private long lastSerdTxRecordCount = 0; + + private MetricsFacade(boolean enabled) { + this.enabled = enabled; + this.debeziumEventCounter = Counter.build() + .name("debezium_event_total") + .help("Debezium Event Total") + .register(); + + this.rowEventCounter = Counter.build() + .name("row_event_total") + .help("Debezium Row Event Total") + .register(); + + this.serdRowRecordCounter = Counter.build() + .name("serd_row_record") + .help("Serialized Row Record Total") + .register(); + + this.serdTxRecordCounter = Counter.build() + .name("serd_tx_record") + .help("Serialized Transaction Record Total") + .register(); + + this.tableChangeCounter = Counter.build() + .name("sink_table_changes_total") + .help("Total processed table changes") + .labelNames("table") + .register(); + + this.rowChangeCounter = Counter.build() + .name("sink_row_changes_total") + .help("Total processed row changes") + .labelNames("table", "operation") + .register(); + + this.transactionCounter = Counter.build() + .name("sink_transactions_total") + .help("Total committed transactions") + .register(); + + this.processingLatency = Summary.build() + .name("sink_processing_latency_seconds") + .help("End-to-end processing latency") + .quantile(0.5, 0.05) + .quantile(0.75, 0.01) + .quantile(0.95, 0.005) + .quantile(0.99, 0.001) + .register(); + + this.rawDataThroughputCounter = Counter.build() + .name("sink_data_throughput_counter") + .help("Data throughput") + .register(); + + this.transServiceLatency = Summary.build() + .name("trans_service_latency_seconds") + .help("End-to-end processing latency") + .quantile(0.5, 0.05) + .quantile(0.75, 0.01) + .quantile(0.95, 0.005) + .quantile(0.99, 0.001) + .register(); + + this.indexServiceLatency = Summary.build() + .name("index_service_latency_seconds") + .help("End-to-end processing latency") + .quantile(0.5, 0.05) + .quantile(0.75, 0.01) + .quantile(0.95, 0.005) + .quantile(0.99, 0.001) + .register(); + + this.retinaServiceLatency = Summary.build() + .name("retina_service_latency_seconds") + .help("End-to-end processing latency") + .quantile(0.5, 0.05) + .quantile(0.75, 0.01) + .quantile(0.95, 0.005) + .quantile(0.99, 0.001) + .register(); + + this.writerLatency = Summary.build() + .name("write_latency_seconds") + .help("Write latency") + .labelNames("table") + .quantile(0.5, 0.05) + .quantile(0.75, 0.01) + .quantile(0.95, 0.005) + .quantile(0.99, 0.001) + .register(); + + this.totalLatency = Summary.build() + .name("total_latency_seconds") + .help("total latency to ETL a row change event") + .labelNames("table", "operation") + .quantile(0.5, 0.05) + .quantile(0.75, 0.01) + .quantile(0.95, 0.005) + .quantile(0.99, 0.001) + .register(); + + this.tableFreshness = Summary.build() + .name("data_freshness_latency_ms") + .help("Data freshness latency in milliseconds per table") + .labelNames("table") + .quantile(0.5, 0.01) + .quantile(0.9, 0.01) + .quantile(0.99, 0.001) + .register(); + + this.transactionRowCountHistogram = Histogram.build() + .name("transaction_row_count_histogram") + .help("Distribution of row counts within a single transaction") + .buckets(1, 5, 10, 20, 30, 40, 50, 60, 70, 80, 90, 100, 200) + .register(); + this.primaryKeyUpdateDistribution = Histogram.build() + .name("primary_key_update_distribution") + .help("Distribution of primary key updates by logical bucket/hash for hot spot analysis") + .labelNames("table") // Table name tag + .buckets(1, 2, 3, 4, 5, 6, 7, 8, 9, 10) // 10 buckets for distribution + .register(); + this.freshness = new SynchronizedDescriptiveStatistics(); + this.rowChangeSpeed = new SynchronizedDescriptiveStatistics(); + + freshnessReportInterval = config.getFreshnessReportInterval(); + freshnessReportPath = config.getMonitorFreshnessReportFile(); + freshnessAvg = new OneSecondAverage(freshnessReportInterval); + freshnessVerbose = config.isSinkMonitorFreshnessVerbose(); + if (freshnessVerbose) { + freshnessHistory = new FreshnessHistory(); + } else { + freshnessHistory = null; + } + + + monitorReportEnabled = config.isMonitorReportEnabled(); + monitorReportInterval = config.getMonitorReportInterval(); + monitorReportPath = config.getMonitorReportFile(); + if (monitorReportEnabled) { + running.set(true); + reportThread = new Thread(this::run, "Metrics Report Thread"); + LOGGER.info("Metrics Report Thread Started"); + reportThread.start(); + freshnessThread = new Thread(this::runFreshness, "Freshness Thread"); + freshnessThread.start(); + } else { + reportThread = null; + freshnessThread = null; + } + } + + private static synchronized void initialize() { + if (instance == null) { + instance = new MetricsFacade(config.isMonitorEnabled()); + LOGGER.info("Init Metrics Facade"); + } + } + + public static MetricsFacade getInstance() { + if (instance == null) { + initialize(); + } + return instance; + } + + public void stop() { + running.set(false); + if (reportThread != null) { + reportThread.interrupt(); + } + + if (freshnessThread != null) { + freshnessThread.interrupt(); + } + LOGGER.info("Monitor report thread stopped."); + } + + public void recordDebeziumEvent() { + if (enabled && debeziumEventCounter != null) { + debeziumEventCounter.inc(); + } + } + + public void recordRowChange(String table, SinkProto.OperationType operation) { + recordRowChange(table, operation, 1); + } + + public void recordRowChange(String table, SinkProto.OperationType operation, int rows) { + if (enabled && rowChangeCounter != null) { + tableChangeCounter.labels(table).inc(rows); + rowChangeCounter.labels(table, operation.toString()).inc(rows); + } + } + + public void recordSerdRowChange() { + recordSerdRowChange(1); + } + + public void recordSerdRowChange(int i) { + if (enabled && serdRowRecordCounter != null) { + serdRowRecordCounter.inc(i); + } + } + + + public void recordSerdTxChange() { + recordSerdTxChange(1); + } + + public void recordSerdTxChange(int i) { + if (enabled && serdTxRecordCounter != null) { + serdTxRecordCounter.inc(i); + } + } + + + public void recordTransaction(int i) { + if (enabled && transactionCounter != null) { + transactionCounter.inc(i); + } + } + + public void recordTransaction() { + recordTransaction(1); + } + + public Summary.Timer startProcessLatencyTimer() { + return enabled ? processingLatency.startTimer() : null; + } + + public Summary.Timer startIndexLatencyTimer() { + return enabled ? indexServiceLatency.startTimer() : null; + } + + public Summary.Timer startTransLatencyTimer() { + return enabled ? transServiceLatency.startTimer() : null; + } + + public Summary.Timer startRetinaLatencyTimer() { + return enabled ? retinaServiceLatency.startTimer() : null; + } + + public Summary.Timer startWriteLatencyTimer(String tableName) { + return enabled ? writerLatency.labels(tableName).startTimer() : null; + } + + public void addRawData(double data) { + rawDataThroughputCounter.inc(data); + } + + public void recordTotalLatency(RowChangeEvent event) { + if (event.getTimeStamp() != 0) { + long recordLatency = System.currentTimeMillis() - event.getTimeStamp(); + totalLatency.labels(event.getFullTableName(), event.getOp().toString()).observe(recordLatency); + } + } + + public void recordRowEvent() { + recordRowEvent(1); + } + + public void recordRowEvent(int i) { + if (enabled && rowEventCounter != null) { + rowEventCounter.inc(i); + } + } + + public int getRecordRowEvent() { + return (int) rowEventCounter.get(); + } + + public int getTransactionEvent() { + return (int) transactionCounter.get(); + } + + public void recordTableFreshness(String table, double freshnessMill) { + if (!enabled) { + return; + } + + tableFreshness.labels(table).observe(freshnessMill); + recordFreshness(freshnessMill); + } + + public void recordFreshness(double freshnessMill) { + if (enabled && freshness != null) { + freshness.addValue(freshnessMill); + } + + if (freshnessAvg != null) { + freshnessAvg.record(freshnessMill); + } + + if (freshnessVerbose) { + freshnessHistory.record(freshnessMill); + } + } + + public void recordPrimaryKeyUpdateDistribution(String table, ByteString pkValue) { + if (!enabled || primaryKeyUpdateDistribution == null) { + return; + } + if (pkValue == null || pkValue.isEmpty()) { + LOGGER.debug("Skipping PK distribution recording: pkValue is null or empty for table {}.", table); + return; + } + + long numericPK; + int length = pkValue.size(); + + try { + ByteBuffer buffer = pkValue.asReadOnlyByteBuffer(); + + if (length == Integer.BYTES) { + numericPK = Integer.toUnsignedLong(buffer.getInt()); + } else if (length == Long.BYTES) { + numericPK = buffer.getLong(); + } else { + LOGGER.warn("Unsupported PK ByteString length {} for table {}. Expected 4 or 8.", length, table); + return; + } + } catch (Exception e) { + LOGGER.error("Failed to convert ByteString to numeric type for table {}: {}", table, e.getMessage()); + return; + } + int hash = Long.hashCode(numericPK); + double bucketIndex = (Math.abs(hash % 10)) + 1; + + // 3. 记录到 Histogram + primaryKeyUpdateDistribution.labels(table).observe(bucketIndex); + + LOGGER.debug("Table {}: PK {} mapped to bucket index {}", table, numericPK, bucketIndex); + } + + public void recordTransactionRowCount(int rowCount) { + if (enabled && transactionRowCountHistogram != null) { + // Use observe() to add the value to the Histogram's configured buckets. + transactionRowCountHistogram.observe(rowCount); + } + } + + public void run() { + while (running.get()) { + try { + Thread.sleep(monitorReportInterval); + logPerformance(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + break; + } catch (Throwable t) { + LOGGER.warn("Error while reporting performance.", t); + } + } + } + + public void runFreshness() { + try { + Thread.sleep(monitorReportInterval); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + while (running.get()) { + try { + Thread.sleep(freshnessReportInterval); + try (FileWriter fw = new FileWriter(freshnessReportPath, true)) { + if (freshnessVerbose) { + List detailedRecords = freshnessHistory.pollAll(); + if (!detailedRecords.isEmpty()) { + for (FreshnessHistory.Record record : detailedRecords) { + fw.write(record.toString() + "\n"); + } + fw.flush(); + } + } else { + long now = System.currentTimeMillis(); + double avg = freshnessAvg.getWindowAverage(); + if (Double.isNaN(avg)) { + continue; + } + fw.write(now + "," + avg + "\n"); + fw.flush(); + } + } catch (IOException e) { + LOGGER.warn("Failed to write perf metrics: " + e.getMessage()); + } + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + break; + } catch (Throwable t) { + LOGGER.warn("Error while reporting performance.", t); + } + } + } + + public void logPerformance() { + long currentRows = (long) rowEventCounter.get(); + long currentTxns = (long) transactionCounter.get(); + long currentDebezium = (long) debeziumEventCounter.get(); + long currentSerdRows = (long) serdRowRecordCounter.get(); + long currentSerdTxs = (long) serdTxRecordCounter.get(); + + long deltaRows = currentRows - lastRowChangeCount; + long deltaTxns = currentTxns - lastTransactionCount; + long deltaDebezium = currentDebezium - lastDebeziumCount; + long deltaSerdRows = currentSerdRows - lastSerdRowRecordCount; + long deltaSerdTxs = currentSerdTxs - lastSerdTxRecordCount; + + lastRowChangeCount = currentRows; + lastTransactionCount = currentTxns; + lastDebeziumCount = currentDebezium; + lastSerdRowRecordCount = currentSerdRows; + lastSerdTxRecordCount = currentSerdTxs; + + double seconds = monitorReportInterval / 1000.0; + + double rowOips = deltaRows / seconds; + double txnOips = deltaTxns / seconds; + double dbOips = deltaDebezium / seconds; + double serdRowsOips = deltaSerdRows / seconds; + double serdTxsOips = deltaSerdTxs / seconds; + + rowChangeSpeed.addValue(rowOips); + + LOGGER.info( + "Performance report: +{} rows (+{}/s), +{} transactions (+{}/s), +{} debezium (+{}/s)" + + ", +{} serdRows (+{}/s), +{} serdTxs (+{}/s)" + + " in {} ms\t activeTxNum: {} min Tx: {}", + deltaRows, String.format("%.2f", rowOips), + deltaTxns, String.format("%.2f", txnOips), + deltaDebezium, String.format("%.2f", dbOips), + deltaSerdRows, String.format("%.2f", serdRowsOips), + deltaSerdTxs, String.format("%.2f", serdTxsOips), + monitorReportInterval, + sinkContextManager.getActiveTxnsNum(), + sinkContextManager.findMinActiveTx() + ); + + LOGGER.info( + String.format( + "Row Per/Second Summary: Max=%.2f, Min=%.2f, Mean=%.2f, P10=%.2f, P50=%.2f, P90=%.2f, P95=%.2f, P99=%.2f", + rowChangeSpeed.getMax(), + rowChangeSpeed.getMin(), + rowChangeSpeed.getMean(), + rowChangeSpeed.getPercentile(10), + rowChangeSpeed.getPercentile(50), + rowChangeSpeed.getPercentile(90), + rowChangeSpeed.getPercentile(95), + rowChangeSpeed.getPercentile(99) + ) + ); + + LOGGER.info( + String.format( + "Freshness Report: Count=%d, Max=%.2f, Min=%.2f, Mean=%.2f, P50=%.2f, P90=%.2f, P95=%.2f, P99=%.2f", + freshness.getN(), + freshness.getMax(), + freshness.getMin(), + freshness.getMean(), + freshness.getPercentile(50), + freshness.getPercentile(90), + freshness.getPercentile(95), + freshness.getPercentile(99) + ) + ); + + String time = LocalDateTime.now().format(DateTimeFormatter.ofPattern("HH:mm:ss")); + // Append to CSV for plotting + try (FileWriter fw = new FileWriter(monitorReportPath, true)) { + fw.write(String.format("%s,%.2f,%.2f,%.2f,%.2f,%.2f%n", + time, rowOips, txnOips, dbOips, serdRowsOips, serdTxsOips)); + } catch (IOException e) { + LOGGER.warn("Failed to write perf metrics: " + e.getMessage()); + } + } +} \ No newline at end of file diff --git a/src/main/java/io/pixelsdb/pixels/sink/util/TableCounters.java b/src/main/java/io/pixelsdb/pixels/sink/util/TableCounters.java new file mode 100644 index 0000000..2adac60 --- /dev/null +++ b/src/main/java/io/pixelsdb/pixels/sink/util/TableCounters.java @@ -0,0 +1,51 @@ +/* + * 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.util; + +/** + * Inner class to hold and manage per-table transaction row counts. + */ +public class TableCounters { + private final int totalCount; // The expected total number of rows + // currentCount is volatile for visibility across threads, as it's incremented during writeRow. + private volatile int currentCount = 0; + + public TableCounters(int totalCount) { + this.totalCount = totalCount; + } + + public void increment() { + currentCount++; + } + + public boolean isComplete() { + // Checks if the processed count meets or exceeds the expected total count. + return currentCount >= totalCount; + } + + public int getCurrentCount() { + return currentCount; + } + + public int getTotalCount() { + return totalCount; + } +} diff --git a/src/main/java/io/pixelsdb/pixels/sink/writer/AbstractBucketedWriter.java b/src/main/java/io/pixelsdb/pixels/sink/writer/AbstractBucketedWriter.java new file mode 100644 index 0000000..6a49495 --- /dev/null +++ b/src/main/java/io/pixelsdb/pixels/sink/writer/AbstractBucketedWriter.java @@ -0,0 +1,103 @@ +/* + * 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.writer; + +import io.pixelsdb.pixels.sink.SinkProto; +import io.pixelsdb.pixels.sink.event.RowChangeEvent; +import io.pixelsdb.pixels.sink.exception.SinkException; + +public abstract class AbstractBucketedWriter { + public void writeRowChangeEvent(RowChangeEvent event, C context) throws SinkException { + if (event == null) { + return; + } + + event.initIndexKey(); + + switch (event.getOp()) { + case UPDATE -> { + if (!event.isPkChanged()) { + emitBefore(event, context); + } else { + emitPkChangedUpdate(event, context); + } + } + + case DELETE -> emitBefore(event, context); + + case INSERT, SNAPSHOT -> emitAfter(event, context); + + case UNRECOGNIZED -> { + return; + } + } + } + + /* ================= hook points ================= */ + + protected void emitBefore(RowChangeEvent event, C context) { + int bucketId = event.getBeforeBucketFromIndex(); + emit(event, bucketId, context); + } + + protected void emitAfter(RowChangeEvent event, C context) { + int bucketId = event.getAfterBucketFromIndex(); + emit(event, bucketId, context); + } + + protected void emitPkChangedUpdate(RowChangeEvent event, C context) throws SinkException { + // DELETE (before) + RowChangeEvent deleteEvent = buildDeleteEvent(event); + emitBefore(deleteEvent, context); + + // INSERT (after) + RowChangeEvent insertEvent = buildInsertEvent(event); + emitAfter(insertEvent, context); + } + + protected abstract void emit(RowChangeEvent event, int bucketId, C context); + + /* ================= helpers ================= */ + + private RowChangeEvent buildDeleteEvent(RowChangeEvent event) throws SinkException { + SinkProto.RowRecord.Builder builder = + event.getRowRecord().toBuilder() + .clearAfter() + .setOp(SinkProto.OperationType.DELETE); + + RowChangeEvent deleteEvent = + new RowChangeEvent(builder.build(), event.getSchema()); + deleteEvent.initIndexKey(); + return deleteEvent; + } + + private RowChangeEvent buildInsertEvent(RowChangeEvent event) throws SinkException { + SinkProto.RowRecord.Builder builder = + event.getRowRecord().toBuilder() + .clearBefore() + .setOp(SinkProto.OperationType.INSERT); + + RowChangeEvent insertEvent = + new RowChangeEvent(builder.build(), event.getSchema()); + insertEvent.initIndexKey(); + return insertEvent; + } +} diff --git a/src/main/java/io/pixelsdb/pixels/sink/writer/NoneWriter.java b/src/main/java/io/pixelsdb/pixels/sink/writer/NoneWriter.java new file mode 100644 index 0000000..ccf5b60 --- /dev/null +++ b/src/main/java/io/pixelsdb/pixels/sink/writer/NoneWriter.java @@ -0,0 +1,170 @@ +/* + * 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.writer; + +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.util.MetricsFacade; +import io.pixelsdb.pixels.sink.writer.retina.SinkContext; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +/** + * NoneWriter implementation used for testing and metrics collection. + * It tracks transaction completeness based on row counts provided in the TXEND metadata, + * ensuring robust handling of out-of-order and concurrent TX BEGIN, TX END, and ROWChange events. + */ +public class NoneWriter implements PixelsSinkWriter { + private static final Logger LOGGER = LoggerFactory.getLogger(NoneWriter.class); + + private final MetricsFacade metricsFacade = MetricsFacade.getInstance(); + + /** + * Data structure to track transaction progress: + * Map + */ + private final Map transTracker = new ConcurrentHashMap<>(); + + /** + * Checks if all tables within a transaction have reached their expected row count. + * If complete, the transaction is removed from the tracker and final metrics are recorded. + * + * @param transId The ID of the transaction to check. + */ + private void checkAndCleanupTransaction(String transId) { + TransactionContext context = transTracker.get(transId); + + if (context == null) { + return; + } + + boolean allComplete = context.sinkContext.isCompleted(); + int actualProcessedRows = context.sinkContext.getProcessedRowsNum(); + + if (allComplete) { + // All rows expected have been processed. Remove and record metrics. + transTracker.remove(transId); + LOGGER.trace("Transaction {} successfully completed and removed from tracker. Total rows: {}.", transId, actualProcessedRows); + + // Record final transaction metrics only upon completion + metricsFacade.recordTransaction(); + metricsFacade.recordTransactionRowCount(actualProcessedRows); + } else { + // Not complete, keep tracking + LOGGER.debug("Transaction {} is partially complete ({} rows processed). Keeping tracker entry.", transId, actualProcessedRows); + } + } + + @Override + public void flush() { + // No-op for NoneWriter + } + + // --- Interface Methods --- + + @Override + public boolean writeRow(RowChangeEvent rowChangeEvent) { + metricsFacade.recordRowEvent(); + metricsFacade.recordRowChange(rowChangeEvent.getTable(), rowChangeEvent.getOp()); + try { + rowChangeEvent.initIndexKey(); + metricsFacade.recordPrimaryKeyUpdateDistribution(rowChangeEvent.getTable(), rowChangeEvent.getAfterKey().getKey()); + + // Get transaction ID and table name + String transId = rowChangeEvent.getTransaction().getId(); + String fullTable = rowChangeEvent.getFullTableName(); + + // 1. Get or create the transaction context + TransactionContext context = transTracker.computeIfAbsent(transId, k -> new TransactionContext(transId)); + + context.sinkContext.getTableCounterLock().lock(); + context.incrementEndCount(fullTable); + checkAndCleanupTransaction(transId); + context.sinkContext.getTableCounterLock().unlock(); + } catch (SinkException e) { + throw new RuntimeException("Error processing row key or metrics.", e); + } + return true; + } + + @Override + public boolean writeTrans(SinkProto.TransactionMetadata transactionMetadata) { + String transId = transactionMetadata.getId(); + + if (transactionMetadata.getStatus() == SinkProto.TransactionStatus.BEGIN) { + // 1. BEGIN: Create context if not exists (in case ROWChange arrived first). + transTracker.computeIfAbsent(transId, k -> new TransactionContext(transId)); + LOGGER.debug("Transaction {} BEGIN received.", transId); + + } else if (transactionMetadata.getStatus() == SinkProto.TransactionStatus.END) { + // 2. END: Finalize tracker state, merge pre-counts, and trigger cleanup. + + // Get existing context or create a new one (in case BEGIN was missed). + TransactionContext context = transTracker.computeIfAbsent(transId, k -> new TransactionContext(transId)); + context.sinkContext.getTableCounterLock().lock(); + context.sinkContext.setEndTx(transactionMetadata); + checkAndCleanupTransaction(transId); + context.sinkContext.getTableCounterLock().unlock(); + } + return true; + } + + @Override + public void close() throws IOException { + // No-op for NoneWriter + LOGGER.info("Remaining unfinished transactions on close: {}", transTracker.size()); + + // Log details of transactions that were never completed + if (!transTracker.isEmpty()) { + transTracker.forEach((transId, context) -> + { + LOGGER.warn("Unfinished transaction {}", transId); + }); + } + } + + /** + * Helper class to manage the state of a single transaction, decoupling the row accumulation + * from the final TableCounters initialization (which requires total counts from TX END). + */ + public static class TransactionContext { + // Key: Full Table Name, Value: Row Count + private SinkContext sinkContext = null; + + + TransactionContext(String txId) { + this.sinkContext = new SinkContext(txId); + } + + + /** + * @param table Full table name + */ + public void incrementEndCount(String table) { + sinkContext.updateCounter(table, 1); + } + } +} \ No newline at end of file diff --git a/src/main/java/io/pixelsdb/pixels/sink/writer/PixelsSinkMode.java b/src/main/java/io/pixelsdb/pixels/sink/writer/PixelsSinkMode.java new file mode 100644 index 0000000..42bea45 --- /dev/null +++ b/src/main/java/io/pixelsdb/pixels/sink/writer/PixelsSinkMode.java @@ -0,0 +1,39 @@ +/* + * 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.writer; + + +public enum PixelsSinkMode { + CSV, + RETINA, + PROTO, + FLINK, + NONE; + + public static PixelsSinkMode fromValue(String value) { + for (PixelsSinkMode mode : values()) { + if (mode.name().equalsIgnoreCase(value)) { + return mode; + } + } + throw new RuntimeException(String.format("Can't convert %s to writer type", value)); + } +} diff --git a/src/main/java/io/pixelsdb/pixels/sink/writer/PixelsSinkWriter.java b/src/main/java/io/pixelsdb/pixels/sink/writer/PixelsSinkWriter.java new file mode 100644 index 0000000..d933a94 --- /dev/null +++ b/src/main/java/io/pixelsdb/pixels/sink/writer/PixelsSinkWriter.java @@ -0,0 +1,35 @@ +/* + * 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.writer; + +import io.pixelsdb.pixels.sink.SinkProto; +import io.pixelsdb.pixels.sink.event.RowChangeEvent; + +import java.io.Closeable; + +public interface PixelsSinkWriter extends Closeable { + void flush(); + + boolean writeRow(RowChangeEvent rowChangeEvent); + + boolean writeTrans(SinkProto.TransactionMetadata transactionMetadata); + +} diff --git a/src/main/java/io/pixelsdb/pixels/sink/writer/PixelsSinkWriterFactory.java b/src/main/java/io/pixelsdb/pixels/sink/writer/PixelsSinkWriterFactory.java new file mode 100644 index 0000000..dd884b5 --- /dev/null +++ b/src/main/java/io/pixelsdb/pixels/sink/writer/PixelsSinkWriterFactory.java @@ -0,0 +1,68 @@ +/* + * 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.writer; + +import io.pixelsdb.pixels.sink.config.PixelsSinkConfig; +import io.pixelsdb.pixels.sink.config.factory.PixelsSinkConfigFactory; +import io.pixelsdb.pixels.sink.writer.csv.CsvWriter; +import io.pixelsdb.pixels.sink.writer.flink.FlinkPollingWriter; +import io.pixelsdb.pixels.sink.writer.proto.ProtoWriter; +import io.pixelsdb.pixels.sink.writer.retina.RetinaWriter; + +import java.io.IOException; + +public class PixelsSinkWriterFactory { + private static final PixelsSinkConfig config = PixelsSinkConfigFactory.getInstance(); + + private static volatile PixelsSinkWriter writer = null; + + + static public PixelsSinkWriter getWriter() { + if (writer == null) { + synchronized (PixelsSinkWriterFactory.class) { + if (writer == null) { + try { + switch (config.getPixelsSinkMode()) { + case CSV: + writer = new CsvWriter(); + break; + case RETINA: + writer = new RetinaWriter(); + break; + case PROTO: + writer = new ProtoWriter(); + break; + case FLINK: + writer = new FlinkPollingWriter(); + break; + case NONE: + writer = new NoneWriter(); + break; + } + } catch (IOException e) { + throw new RuntimeException("Can't create writer", e); + } + } + } + } + return writer; + } +} diff --git a/src/main/java/io/pixelsdb/pixels/sink/sink/CsvWriter.java b/src/main/java/io/pixelsdb/pixels/sink/writer/csv/CsvWriter.java similarity index 82% rename from src/main/java/io/pixelsdb/pixels/sink/sink/CsvWriter.java rename to src/main/java/io/pixelsdb/pixels/sink/writer/csv/CsvWriter.java index 536bed8..f3baf07 100644 --- a/src/main/java/io/pixelsdb/pixels/sink/sink/CsvWriter.java +++ b/src/main/java/io/pixelsdb/pixels/sink/writer/csv/CsvWriter.java @@ -1,20 +1,24 @@ /* * 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 + * This file is part of Pixels. * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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. * - * 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. + * 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.sink; + +package io.pixelsdb.pixels.sink.writer.csv; import com.google.common.util.concurrent.ThreadFactoryBuilder; import io.pixelsdb.pixels.sink.SinkProto; @@ -22,6 +26,8 @@ import io.pixelsdb.pixels.sink.config.PixelsSinkDefaultConfig; import io.pixelsdb.pixels.sink.config.factory.PixelsSinkConfigFactory; import io.pixelsdb.pixels.sink.event.RowChangeEvent; +import io.pixelsdb.pixels.sink.writer.PixelsSinkMode; +import io.pixelsdb.pixels.sink.writer.PixelsSinkWriter; import lombok.Getter; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -44,10 +50,9 @@ public class CsvWriter implements PixelsSinkWriter { private static final Logger log = LoggerFactory.getLogger(CsvWriter.class); - private Long recordCnt = 0L; - @Getter private static final PixelsSinkMode pixelsSinkMode = PixelsSinkMode.CSV; + private static final PixelsSinkConfig config = PixelsSinkConfigFactory.getInstance(); private final ReentrantLock lock = new ReentrantLock(); private final ConcurrentMap tableWriters = new ConcurrentHashMap<>(); private final ScheduledExecutorService flushScheduler; @@ -57,9 +62,8 @@ public class CsvWriter implements PixelsSinkWriter { private final ReentrantLock globalLock = new ReentrantLock(); private final ReentrantLock writeLock = new ReentrantLock(true); private final AtomicInteger writeCounter = new AtomicInteger(0); - - private static final PixelsSinkConfig config = PixelsSinkConfigFactory.getInstance(); private final String CSV_DELIMITER = "|"; + private final Long recordCnt = 0L; public CsvWriter() throws IOException { this.databaseName = config.getCaptureDatabase(); @@ -94,7 +98,7 @@ public void flush() { } @Override - public boolean write(RowChangeEvent event) { + public boolean writeRow(RowChangeEvent event) { final String tableName = event.getTable(); if (event.getOp() == SinkProto.OperationType.DELETE) { return true; @@ -123,9 +127,16 @@ public boolean write(RowChangeEvent event) { } } + @Override + public boolean writeTrans(SinkProto.TransactionMetadata transactionMetadata) { + // TODO(AntiO2): Write Trans info + return false; + } + private FileChannel getOrCreateChannel(RowChangeEvent event) throws IOException { String tableName = event.getTable(); - return tableWriters.computeIfAbsent(tableName, key -> { + return tableWriters.computeIfAbsent(tableName, key -> + { try { Path tablePath = baseOutputPath.resolve(tableName + ".csv"); FileChannel channel = FileChannel.open(tablePath, @@ -146,7 +157,8 @@ private FileChannel getOrCreateChannel(RowChangeEvent event) throws IOException private String convertToCSV(Map message) { return message.values().stream() - .map(obj -> { + .map(obj -> + { if (obj == null) return ""; return obj.toString(); }) @@ -154,6 +166,7 @@ private String convertToCSV(Map message) { } private List getHeaderFields(RowChangeEvent event) { + return event.getSchema().getFieldNames(); } diff --git a/src/main/java/io/pixelsdb/pixels/sink/writer/flink/FlinkPollingWriter.java b/src/main/java/io/pixelsdb/pixels/sink/writer/flink/FlinkPollingWriter.java new file mode 100644 index 0000000..4fdb4ae --- /dev/null +++ b/src/main/java/io/pixelsdb/pixels/sink/writer/flink/FlinkPollingWriter.java @@ -0,0 +1,220 @@ +/* + * 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.writer.flink; + +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.PixelsSinkConstants; +import io.pixelsdb.pixels.sink.config.factory.PixelsSinkConfigFactory; +import io.pixelsdb.pixels.sink.event.RowChangeEvent; +import io.pixelsdb.pixels.sink.writer.AbstractBucketedWriter; +import io.pixelsdb.pixels.sink.writer.PixelsSinkWriter; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.TimeUnit; + +/** + * FlinkPollingWriter is a PixelsSinkWriter implementation designed for a long-polling pattern. + * It maintains in-memory blocking queues per table, acting as a buffer between the upstream + * data source (producer) and the gRPC service (consumer). + * This class is thread-safe and integrates FlushRateLimiter to control ingress traffic. + * It also manages the lifecycle of the gRPC server. + */ +public class FlinkPollingWriter extends AbstractBucketedWriter implements PixelsSinkWriter { + + private static final Logger LOGGER = LoggerFactory.getLogger(FlinkPollingWriter.class); + // Core data structure: A thread-safe map from table name to a thread-safe blocking queue. + private final Map> tableQueues; + // The gRPC server instance managed by this writer. + private final PollingRpcServer pollingRpcServer; + + /** + * Constructor for FlinkPollingWriter. + * Initializes the data structures, rate limiter, and starts the gRPC server. + */ + public FlinkPollingWriter() { + this.tableQueues = new ConcurrentHashMap<>(); + + // --- START: New logic to initialize and start the gRPC server --- + try { + // 1. Get configuration + PixelsSinkConfig config = PixelsSinkConfigFactory.getInstance(); + int rpcPort = config.getSinkFlinkServerPort(); + // 2. Create the gRPC service implementation first, passing a reference to this writer. + PixelsPollingServiceImpl service = new PixelsPollingServiceImpl(this); + + // 3. Create the PollingRpcServer instance with the service and port. + LOGGER.info("Attempting to start gRPC Polling Server on port {}...", rpcPort); + this.pollingRpcServer = new PollingRpcServer(service, rpcPort); + // 4. Start the server. + this.pollingRpcServer.start(); + LOGGER.info("gRPC Polling Server successfully started and is managed by FlinkPollingWriter."); + } catch (IOException e) { + // If the server fails to start, the writer cannot function. + // Throw a RuntimeException to fail the Flink task initialization. + LOGGER.error("Failed to start gRPC server during FlinkPollingWriter initialization.", e); + throw new RuntimeException("Could not start gRPC server", e); + } + // --- END: New logic --- + } + + /** + * [Producer side] Receives row change events from the data source, applies rate limiting, + * converts them, and places them into the in-memory queue. + * + * @param event The row change event + * @return always returns true, unless an interruption occurs. + */ + @Override + public boolean writeRow(RowChangeEvent event) { + if (event == null) { + LOGGER.warn("Received a null RowChangeEvent, skipping."); + return false; + } + + try { + writeRowChangeEvent(event, null); + return true; + } catch (Exception e) { + LOGGER.error( + "Failed to process and write row for table: {}", + event.getFullTableName(), + e + ); + return false; + } + } + + /** + * [Consumer side] The gRPC service calls this method to pull data. + * Implements long-polling logic: if the queue is empty, it blocks for a specified timeout. + * batchSize acts as an upper limit on the number of records pulled to prevent oversized RPC responses. + * + * @param tableName The name of the table to pull data from + * @param bucketId + * @param batchSize The maximum number of records to pull + * @param timeout The maximum time to wait for data + * @param unit The time unit for the timeout + * @return A list of RowRecords, which will be empty if no data is available before the timeout. + * @throws InterruptedException if the thread is interrupted while waiting + */ + public List pollRecords( + SchemaTableName tableName, + int bucketId, + int batchSize, + long timeout, + TimeUnit unit + ) throws InterruptedException { + List records = new ArrayList<>(batchSize); + TableBucketKey key = new TableBucketKey(tableName, bucketId); + + BlockingQueue queue = tableQueues.get(key); + + if (queue == null) { + unit.sleep(timeout); + return records; + } + + SinkProto.RowRecord first = queue.poll(timeout, unit); + if (first == null) { + return records; + } + + records.add(first); + queue.drainTo(records, batchSize - 1); + + LOGGER.info( + "Polled {} records for table {}, bucket {}", + records.size(), tableName, bucketId + ); + return records; + } + + /** + * This implementation does not involve transactions, so this method is a no-op. + */ + @Override + public boolean writeTrans(SinkProto.TransactionMetadata transactionMetadata) { + return true; + } + + /** + * This implementation uses an in-memory queue, so data is immediately available. flush is a no-op. + */ + @Override + public void flush() { + // No-op + } + + /** + * Cleans up resources on close. This is where we stop the gRPC server. + */ + @Override + public void close() throws IOException { + LOGGER.info("Closing FlinkPollingWriter..."); + if (this.pollingRpcServer != null) { + LOGGER.info("Attempting to shut down the gRPC Polling Server..."); + this.pollingRpcServer.stop(); + LOGGER.info("gRPC Polling Server shut down."); + } + LOGGER.info("Clearing all table queues."); + tableQueues.clear(); + LOGGER.info("FlinkPollingWriter closed."); + } + + @Override + protected void emit(RowChangeEvent event, int bucketId, Void unused) { + TableBucketKey key = + new TableBucketKey(event.getSchemaTableName(), bucketId); + + BlockingQueue queue = + tableQueues.computeIfAbsent( + key, + k -> new LinkedBlockingQueue<>(PixelsSinkConstants.MAX_QUEUE_SIZE) + ); + + try { + queue.put(event.getRowRecord()); + LOGGER.debug( + "Enqueued row for table {}, bucket {}, queueSize={}", + event.getFullTableName(), bucketId, queue.size() + ); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new RuntimeException( + "Interrupted while enqueueing row for " + event.getFullTableName(), + e + ); + } + } + + record TableBucketKey(SchemaTableName table, int bucketId) { + } +} diff --git a/src/main/java/io/pixelsdb/pixels/sink/writer/flink/PixelsPollingServiceImpl.java b/src/main/java/io/pixelsdb/pixels/sink/writer/flink/PixelsPollingServiceImpl.java new file mode 100644 index 0000000..94ed1aa --- /dev/null +++ b/src/main/java/io/pixelsdb/pixels/sink/writer/flink/PixelsPollingServiceImpl.java @@ -0,0 +1,105 @@ +/* + * 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.writer.flink; + +import io.grpc.stub.StreamObserver; +import io.pixelsdb.pixels.common.metadata.SchemaTableName; +import io.pixelsdb.pixels.sink.PixelsPollingServiceGrpc; +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.FlushRateLimiter; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.TimeUnit; + +public class PixelsPollingServiceImpl extends PixelsPollingServiceGrpc.PixelsPollingServiceImplBase { + private static final Logger LOGGER = LoggerFactory.getLogger(PixelsPollingServiceImpl.class); + private final FlinkPollingWriter writer; + private final int pollBatchSize; + private final long pollTimeoutMs; + private final FlushRateLimiter flushRateLimiter; + + public PixelsPollingServiceImpl(FlinkPollingWriter writer) { + if (writer == null) { + throw new IllegalArgumentException("FlinkPollingWriter cannot be null."); + } + this.writer = writer; + PixelsSinkConfig config = PixelsSinkConfigFactory.getInstance(); + this.pollBatchSize = config.getCommitBatchSize(); + this.pollTimeoutMs = config.getTimeoutMs(); + this.flushRateLimiter = FlushRateLimiter.getInstance(); + LOGGER.info("PixelsPollingServiceImpl initialized. Using 'sink.commit.batch.size' for pollBatchSize ({}) " + + "and 'sink.timeout.ms' for pollTimeoutMs ({}).", + this.pollBatchSize, this.pollTimeoutMs); + } + + @Override + public void pollEvents(SinkProto.PollRequest request, StreamObserver responseObserver) { + SchemaTableName schemaTableName = new SchemaTableName(request.getSchemaName(), request.getTableName()); + LOGGER.debug("Received poll request for table '{}'", schemaTableName); + List records = new ArrayList<>(pollBatchSize); + + try { + for (int bucketId : request.getBucketsList()) { + if (records.size() >= pollBatchSize) { + break; + } + + List polled = + writer.pollRecords( + schemaTableName, + bucketId, + pollBatchSize - records.size(), + 0, + TimeUnit.MILLISECONDS + ); + + if (polled != null && !polled.isEmpty()) { + records.addAll(polled); + } + } + + SinkProto.PollResponse.Builder responseBuilder = SinkProto.PollResponse.newBuilder(); + if (records != null && !records.isEmpty()) { + responseBuilder.addAllRecords(records); + this.flushRateLimiter.acquire(records.size()); + } + + responseObserver.onNext(responseBuilder.build()); + responseObserver.onCompleted(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + LOGGER.error("Polling thread was interrupted for table: " + schemaTableName, e); + responseObserver.onError(io.grpc.Status.INTERNAL + .withDescription("Server polling was interrupted") + .asRuntimeException()); + } catch (Exception e) { + LOGGER.error("An unexpected error occurred while polling for table: " + schemaTableName, e); + responseObserver.onError(io.grpc.Status.UNKNOWN + .withDescription("An unexpected error occurred: " + e.getMessage()) + .asRuntimeException()); + } + } +} \ No newline at end of file diff --git a/src/main/java/io/pixelsdb/pixels/sink/writer/flink/PollingRpcServer.java b/src/main/java/io/pixelsdb/pixels/sink/writer/flink/PollingRpcServer.java new file mode 100644 index 0000000..4e95836 --- /dev/null +++ b/src/main/java/io/pixelsdb/pixels/sink/writer/flink/PollingRpcServer.java @@ -0,0 +1,74 @@ +/* + * 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.writer.flink; + +import io.grpc.Server; +import io.grpc.ServerBuilder; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.util.concurrent.TimeUnit; + +public class PollingRpcServer { + + private static final Logger LOGGER = LoggerFactory.getLogger(PollingRpcServer.class); + private final Server server; + private final int port; + + public PollingRpcServer(PixelsPollingServiceImpl serviceImpl, int port) { + this.port = port; + this.server = ServerBuilder.forPort(port) + .addService(serviceImpl) // 将具体的服务实现绑定到服务器 + .build(); + } + + public void start() throws IOException { + server.start(); + LOGGER.info("gRPC Polling Server started, listening on port " + port); + } + + public void stop() { + LOGGER.info("Attempting to shut down gRPC Polling Server..."); + if (server != null) { + try { + if (!server.isTerminated()) { + server.shutdown().awaitTermination(5, TimeUnit.SECONDS); + } + } catch (InterruptedException e) { + LOGGER.error("gRPC server shutdown interrupted.", e); + Thread.currentThread().interrupt(); + } finally { + if (!server.isTerminated()) { + LOGGER.warn("gRPC server did not terminate gracefully. Forcing shutdown."); + server.shutdownNow(); + } + } + } + LOGGER.info("gRPC Polling Server shut down."); + } + + public void awaitTermination() throws InterruptedException { + if (server != null) { + server.awaitTermination(); + } + } +} diff --git a/src/main/java/io/pixelsdb/pixels/sink/writer/proto/ProtoWriter.java b/src/main/java/io/pixelsdb/pixels/sink/writer/proto/ProtoWriter.java new file mode 100644 index 0000000..d451332 --- /dev/null +++ b/src/main/java/io/pixelsdb/pixels/sink/writer/proto/ProtoWriter.java @@ -0,0 +1,302 @@ +/* + * 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.writer.proto; + + +import io.pixelsdb.pixels.common.physical.PhysicalWriter; +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.TableCounters; +import io.pixelsdb.pixels.sink.writer.PixelsSinkWriter; +import lombok.Getter; +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.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.locks.ReentrantLock; + +/** + * @package: io.pixelsdb.pixels.sink.writer + * @className: ProtoWriter + * @author: AntiO2 + * @date: 2025/10/5 07:10 + */ +public class ProtoWriter implements PixelsSinkWriter { + private final Logger LOGGER = LoggerFactory.getLogger(ProtoWriter.class); + private final RotatingWriterManager writerManager; + private final TableMetadataRegistry instance; + private final ReentrantLock lock = new ReentrantLock(); + /** + * Data structure to track transaction progress: + * Map + */ + private final Map transTracker = new ConcurrentHashMap<>(); + + + public ProtoWriter() throws IOException { + PixelsSinkConfig sinkConfig = PixelsSinkConfigFactory.getInstance(); + + String dataPath = sinkConfig.getSinkProtoData(); + this.writerManager = new RotatingWriterManager(dataPath); + this.instance = TableMetadataRegistry.Instance(); + } + + /** + * Checks if all tables within a transaction have reached their expected row count. + * If complete, the transaction is removed from the tracker and final metrics are recorded. + * + * @param transId The ID of the transaction to check. + */ + private void checkAndCleanupTransaction(String transId) { + TransactionContext context = transTracker.get(transId); + + if (context == null || !context.isEndReceived()) { + // Transaction has not received TX END or has been cleaned up already. + return; + } + + Map tableMap = context.tableCounters; + if (tableMap == null || tableMap.isEmpty()) { + // Empty transaction with no tables. Clean up immediately. + transTracker.remove(transId); + LOGGER.info("Transaction {} (empty) successfully completed and removed from tracker.", transId); + return; + } + + boolean allComplete = true; + int actualProcessedRows = 0; + + // Iterate through all tables to check completion status + for (Map.Entry entry : tableMap.entrySet()) { + TableCounters counters = entry.getValue(); + if (!counters.isComplete()) { + allComplete = false; + } + } + + if (allComplete) { + transTracker.remove(transId); + ByteBuffer transInfo = getTransBuffer(context); + transInfo.rewind(); + writeBuffer(transInfo); + } + } + + @Override + public boolean writeTrans(SinkProto.TransactionMetadata transactionMetadata) { + try { + lock.lock(); + String transId = transactionMetadata.getId(); + if (transactionMetadata.getStatus() == SinkProto.TransactionStatus.BEGIN) { + // 1. BEGIN: Create context if not exists (in case ROWChange arrived first). + TransactionContext transactionContext = transTracker.computeIfAbsent(transId, k -> new TransactionContext()); + LOGGER.debug("Transaction {} BEGIN received.", transId); + transactionContext.txBegin = transactionMetadata; + } else if (transactionMetadata.getStatus() == SinkProto.TransactionStatus.END) { + // 2. END: Finalize tracker state, merge pre-counts, and trigger cleanup. + + // Get existing context or create a new one (in case BEGIN was missed). + TransactionContext context = transTracker.computeIfAbsent(transId, k -> new TransactionContext()); + + // --- Initialization Step: Set Total Counts --- + Map newTableCounters = new ConcurrentHashMap<>(); + for (SinkProto.DataCollection dataCollection : transactionMetadata.getDataCollectionsList()) { + String fullTable = dataCollection.getDataCollection(); + // Create official counter with total count + newTableCounters.put(fullTable, new TableCounters((int) dataCollection.getEventCount())); + } + + // Set the final state (must be volatile write) + context.setEndReceived(newTableCounters); + + // --- Merge Step: Apply pre-received rows --- + for (Map.Entry preEntry : context.preEndCounts.entrySet()) { + String table = preEntry.getKey(); + int accumulatedCount = preEntry.getValue().get(); + TableCounters finalCounter = newTableCounters.get(table); + + if (finalCounter != null) { + // Apply the accumulated count to the official counter + for (int i = 0; i < accumulatedCount; i++) { + finalCounter.increment(); + } + } else { + LOGGER.warn("Pre-received rows for table {} (count: {}) but table was not in TX END metadata. Discarding accumulated count.", table, accumulatedCount); + } + } + context.txEnd = transactionMetadata; + + // --- Cleanup/Validation Step --- + // Trigger cleanup. This will validate if all rows (pre and post END) have satisfied the total counts. + checkAndCleanupTransaction(transId); + } + return true; + } finally { + lock.unlock(); + } + } + + private ByteBuffer getTransBuffer(TransactionContext transactionContext) { + int total = 0; + byte[] transDataBegin = transactionContext.txBegin.toByteArray(); + ByteBuffer beginByteBuffer = writeData(-1, transDataBegin); + total += beginByteBuffer.limit(); + beginByteBuffer.rewind(); + byte[] transDataEnd = transactionContext.txEnd.toByteArray(); + ByteBuffer endByteBuffer = writeData(-1, transDataEnd); + endByteBuffer.rewind(); + total += endByteBuffer.limit(); + List rowEvents = new ArrayList<>(); + for (RowChangeEvent rowChangeEvent : transactionContext.rowChangeEventList) { + ByteBuffer byteBuffer = write(rowChangeEvent.getRowRecord()); + if (byteBuffer == null) { + return null; + } + byteBuffer.rewind(); + rowEvents.add(byteBuffer); + total += byteBuffer.limit(); + } + ByteBuffer buffer = ByteBuffer.allocate(total); + buffer.put(beginByteBuffer.array()); + for (ByteBuffer rowEvent : rowEvents) { + buffer.put(rowEvent.array()); + } + buffer.put(endByteBuffer.array()); + return buffer; + } + + public ByteBuffer write(SinkProto.RowRecord rowRecord) { + byte[] rowData = rowRecord.toByteArray(); + String tableName = rowRecord.getSource().getTable(); + String schemaName = rowRecord.getSource().getDb(); + + long tableId; + try { + tableId = instance.getTableId(schemaName, tableName); + } catch (SinkException e) { + LOGGER.error("Error while getting schema table id.", e); + return null; + } + { + return writeData((int) tableId, rowData); + } + } + + // key: -1 means transaction, else means table id + private ByteBuffer writeData(int key, byte[] data) { + ByteBuffer buf = ByteBuffer.allocate(Integer.BYTES + Integer.BYTES + data.length).order(ByteOrder.BIG_ENDIAN); // key + value len + data + buf.putInt(key).putInt(data.length).put(data); + return buf; + } + + private synchronized boolean writeBuffer(ByteBuffer buf) { + PhysicalWriter writer; + try { + writer = writerManager.current(); + writer.prepare(buf.remaining()); + writer.append(buf.array()); + } catch (IOException e) { + LOGGER.error("Error while writing row record.", e); + return false; + } + return true; + } + + @Override + public boolean writeRow(RowChangeEvent rowChangeEvent) { + try { + lock.lock(); + String transId = rowChangeEvent.getTransaction().getId(); + String fullTable = rowChangeEvent.getFullTableName(); + + // 1. Get or create the transaction context + TransactionContext context = transTracker.computeIfAbsent(transId, k -> new TransactionContext()); + context.rowChangeEventList.add(rowChangeEvent); + // 2. Check if TX END has arrived + if (context.isEndReceived()) { + // TX END arrived: Use official TableCounters + TableCounters counters = context.tableCounters.get(fullTable); + if (counters != null) { + // Increment the processed row count for this table + counters.increment(); + + // If this table completed, check if the entire transaction is complete. + if (counters.isComplete()) { + checkAndCleanupTransaction(transId); + } + } else { + LOGGER.warn("Row received for TransId {} / Table {} but was not included in TX END metadata.", transId, fullTable); + } + } else { + context.incrementPreEndCount(fullTable); + LOGGER.debug("Row received for TransId {} / Table {} before TX END. Accumulating count.", transId, fullTable); + } + return true; + } finally { + lock.unlock(); + } + } + + @Override + public void flush() { + + } + + @Override + public void close() throws IOException { + this.writerManager.close(); + } + + private static class TransactionContext { + // Key: Full Table Name, Value: Row Count + private final Map preEndCounts = new ConcurrentHashMap<>(); + public List rowChangeEventList = new ArrayList<>(); + public SinkProto.TransactionMetadata txBegin; + public SinkProto.TransactionMetadata txEnd; + @Getter + private volatile boolean endReceived = false; + // Key: Full Table Name + private Map tableCounters = null; + + public void setEndReceived(Map counters) { + this.tableCounters = counters; + this.endReceived = true; + } + + /** + * @param table Full table name + */ + public void incrementPreEndCount(String table) { + preEndCounts.computeIfAbsent(table, k -> new AtomicInteger(0)).incrementAndGet(); + } + } +} \ No newline at end of file diff --git a/src/main/java/io/pixelsdb/pixels/sink/writer/proto/RotatingWriterManager.java b/src/main/java/io/pixelsdb/pixels/sink/writer/proto/RotatingWriterManager.java new file mode 100644 index 0000000..32dd2a9 --- /dev/null +++ b/src/main/java/io/pixelsdb/pixels/sink/writer/proto/RotatingWriterManager.java @@ -0,0 +1,85 @@ +/* + * 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.writer.proto; + + +import io.pixelsdb.pixels.common.physical.PhysicalWriter; +import io.pixelsdb.pixels.common.physical.PhysicalWriterUtil; +import io.pixelsdb.pixels.common.physical.Storage; +import io.pixelsdb.pixels.sink.config.PixelsSinkConfig; +import io.pixelsdb.pixels.sink.config.factory.PixelsSinkConfigFactory; +import io.pixelsdb.pixels.sink.util.EtcdFileRegistry; + +import java.io.IOException; + +/** + * @package: io.pixelsdb.pixels.sink.writer + * @className: RotatingWriterManager + * @author: AntiO2 + * @date: 2025/10/5 07:34 + */ +public class RotatingWriterManager { + private final String baseDir; + private final String topic; + private final int maxRecordsPerFile; + private final Storage.Scheme scheme; + private final EtcdFileRegistry registry; + private int currentCount = 0; + private PhysicalWriter currentWriter; + private String currentFileName; + + public RotatingWriterManager(String topic) throws IOException { + PixelsSinkConfig sinkConfig = PixelsSinkConfigFactory.getInstance(); + this.baseDir = sinkConfig.getSinkProtoDir(); + this.topic = topic; + this.maxRecordsPerFile = sinkConfig.getMaxRecordsPerFile(); + this.registry = new EtcdFileRegistry(topic, baseDir); + this.scheme = Storage.Scheme.fromPath(this.baseDir); + rotate(); + } + + private void rotate() throws IOException { + if (currentWriter != null) { + currentWriter.close(); + registry.markFileCompleted(registry.getCurrentFileKey()); + } + + currentFileName = registry.createNewFile(); + currentWriter = PhysicalWriterUtil.newPhysicalWriter(scheme, currentFileName); + + currentCount = 0; + } + + public PhysicalWriter current() throws IOException { + if (currentCount >= maxRecordsPerFile) { + rotate(); + } + currentCount++; + return currentWriter; + } + + public void close() throws IOException { + if (currentWriter != null) { + currentWriter.close(); + registry.markFileCompleted(registry.getCurrentFileKey()); + } + } +} \ No newline at end of file diff --git a/src/main/java/io/pixelsdb/pixels/sink/writer/retina/RetinaBucketDispatcher.java b/src/main/java/io/pixelsdb/pixels/sink/writer/retina/RetinaBucketDispatcher.java new file mode 100644 index 0000000..ab4bcfe --- /dev/null +++ b/src/main/java/io/pixelsdb/pixels/sink/writer/retina/RetinaBucketDispatcher.java @@ -0,0 +1,39 @@ +/* + * 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.writer.retina; + +import io.pixelsdb.pixels.sink.event.RowChangeEvent; +import io.pixelsdb.pixels.sink.writer.AbstractBucketedWriter; + +public class RetinaBucketDispatcher extends AbstractBucketedWriter { + private final TableWriterProxy tableWriterProxy; + + public RetinaBucketDispatcher() { + this.tableWriterProxy = TableWriterProxy.getInstance(); + } + + @Override + protected void emit(RowChangeEvent event, int bucketId, SinkContext ctx) { + tableWriterProxy + .getTableWriter(event.getTable(), event.getTableId(), bucketId) + .write(event, ctx); + } +} diff --git a/src/main/java/io/pixelsdb/pixels/sink/writer/retina/RetinaServiceProxy.java b/src/main/java/io/pixelsdb/pixels/sink/writer/retina/RetinaServiceProxy.java new file mode 100644 index 0000000..245e309 --- /dev/null +++ b/src/main/java/io/pixelsdb/pixels/sink/writer/retina/RetinaServiceProxy.java @@ -0,0 +1,128 @@ +/* + * 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.writer.retina; + +import io.pixelsdb.pixels.common.exception.RetinaException; +import io.pixelsdb.pixels.common.node.BucketCache; +import io.pixelsdb.pixels.common.retina.RetinaService; +import io.pixelsdb.pixels.common.utils.RetinaUtils; +import io.pixelsdb.pixels.retina.RetinaProto; +import io.pixelsdb.pixels.sink.config.PixelsSinkConfig; +import io.pixelsdb.pixels.sink.config.factory.PixelsSinkConfigFactory; +import io.pixelsdb.pixels.sink.util.MetricsFacade; +import io.pixelsdb.pixels.sink.writer.PixelsSinkMode; +import lombok.Getter; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.atomic.AtomicBoolean; + +public class RetinaServiceProxy { + private static final Logger LOGGER = LoggerFactory.getLogger(RetinaServiceProxy.class); + @Getter + private static final PixelsSinkMode pixelsSinkMode = PixelsSinkMode.RETINA; + private static final PixelsSinkConfig config = PixelsSinkConfigFactory.getInstance(); + // private static final IndexService indexService = IndexService.Instance(); + private final AtomicBoolean isClosed = new AtomicBoolean(false); + private final RetinaService retinaService; + private final MetricsFacade metricsFacade = MetricsFacade.getInstance(); + private final int vNodeId; + private RetinaService.StreamHandler retinaStream = null; + + public RetinaServiceProxy(int bucketId) { + if (bucketId == -1) { + this.retinaService = RetinaService.Instance(); + } else { + this.retinaService = RetinaUtils.getRetinaServiceFromBucketId(bucketId); + } + + + if (config.getRetinaWriteMode() == RetinaWriteMode.STREAM) { + retinaStream = retinaService.startUpdateStream(); + } else { + retinaStream = null; + } + + this.vNodeId = BucketCache.getInstance().getRetinaNodeInfoByBucketId(bucketId).getVirtualNodeId(); + } + + public boolean writeTrans(String schemaName, List tableUpdateData) { + if (config.getRetinaWriteMode() == RetinaWriteMode.STUB) { + try { + retinaService.updateRecord(schemaName, vNodeId, tableUpdateData); + } catch (RetinaException e) { + e.printStackTrace(); + return false; + } + } else { + try { + retinaStream.updateRecord(schemaName, vNodeId, tableUpdateData); + } catch (RetinaException e) { + e.printStackTrace(); + return false; + } + } + return true; + } + + public CompletableFuture writeBatchAsync + (String schemaName, List tableUpdateData) { + if (config.getRetinaWriteMode() == RetinaWriteMode.STUB) { + try { + retinaService.updateRecord(schemaName, vNodeId, tableUpdateData); + } catch (RetinaException e) { + e.printStackTrace(); + } + return null; + } else { + try { + return retinaStream.updateRecord(schemaName, vNodeId, tableUpdateData); + } catch (RetinaException e) { + e.printStackTrace(); + } + return null; + } + } + + public void close() throws IOException { + isClosed.compareAndSet(false, true); + if (config.getRetinaWriteMode() == RetinaWriteMode.STREAM) { + retinaStream.close(); + } + } + + public enum RetinaWriteMode { + STREAM, + STUB; + + public static RetinaWriteMode fromValue(String value) { + for (RetinaWriteMode mode : values()) { + if (mode.name().equalsIgnoreCase(value)) { + return mode; + } + } + throw new RuntimeException(String.format("Can't convert %s to Retina writer type", value)); + } + } +} diff --git a/src/main/java/io/pixelsdb/pixels/sink/writer/retina/RetinaWriter.java b/src/main/java/io/pixelsdb/pixels/sink/writer/retina/RetinaWriter.java new file mode 100644 index 0000000..6ecc33c --- /dev/null +++ b/src/main/java/io/pixelsdb/pixels/sink/writer/retina/RetinaWriter.java @@ -0,0 +1,175 @@ +/* + * 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.writer.retina; + +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.util.FlushRateLimiter; +import io.pixelsdb.pixels.sink.util.MetricsFacade; +import io.pixelsdb.pixels.sink.writer.PixelsSinkWriter; +import org.apache.commons.lang3.RandomUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.atomic.AtomicBoolean; + +public class RetinaWriter implements PixelsSinkWriter { + private static final Logger LOGGER = LoggerFactory.getLogger(RetinaWriter.class); + final ExecutorService dispatchExecutor = Executors.newCachedThreadPool(); + private final ScheduledExecutorService timeoutScheduler = + Executors.newSingleThreadScheduledExecutor(); + private final FlushRateLimiter flushRateLimiter; + private final MetricsFacade metricsFacade = MetricsFacade.getInstance(); + private final SinkContextManager sinkContextManager; + private final TransactionMode transactionMode; + + public RetinaWriter() { + PixelsSinkConfig config = PixelsSinkConfigFactory.getInstance(); + this.sinkContextManager = SinkContextManager.getInstance(); + this.flushRateLimiter = FlushRateLimiter.getInstance(); + this.transactionMode = config.getTransactionMode(); + } + + @Override + public boolean writeTrans(SinkProto.TransactionMetadata txMeta) { + if (transactionMode.equals(TransactionMode.RECORD)) { + return true; + } + + try { + if (txMeta.getStatus() == SinkProto.TransactionStatus.BEGIN) { + handleTxBegin(txMeta); + } else if (txMeta.getStatus() == SinkProto.TransactionStatus.END) { + handleTxEnd(txMeta); + } + } catch (SinkException e) { + LOGGER.error(e.getMessage(), e); + return false; + } + return true; + } + + @Override + public boolean writeRow(RowChangeEvent event) { + try { + if (event == null) { + return false; + } + + metricsFacade.recordRowChange(event.getTable(), event.getOp()); + event.startLatencyTimer(); + if (event.getTransaction() == null || event.getTransaction().getId().isEmpty()) { + handleNonTxEvent(event); + return true; + } + + + String table = event.getFullTableName(); + + long collectionOrder = event.getTransaction().getDataCollectionOrder(); + long totalOrder = event.getTransaction().getTotalOrder(); + if (transactionMode.equals(TransactionMode.RECORD)) { + sinkContextManager.writeRowChangeEvent(null, event); + } else { + AtomicBoolean canWrite = new AtomicBoolean(false); + SinkContext ctx = sinkContextManager.getActiveTxContext(event, canWrite); + + if (canWrite.get()) { + sinkContextManager.writeRowChangeEvent(ctx, event); + } + } + } catch (SinkException e) { + LOGGER.error(e.getMessage(), e); + return false; + } + + return true; + } + + private void handleTxBegin(SinkProto.TransactionMetadata txBegin) throws SinkException { + // startTrans(txBegin.getId()).get(); + try { + // flushRateLimiter.acquire(1); + startTransSync(txBegin.getId()); + } catch (SinkException e) { + throw new SinkException("Failed to start trans", e); + } + + } + + private void startTransSync(String sourceTxId) throws SinkException { + sinkContextManager.startTransSync(sourceTxId); + } + + private void handleTxEnd(SinkProto.TransactionMetadata txEnd) { + sinkContextManager.processTxCommit(txEnd); + } + + private void handleNonTxEvent(RowChangeEvent event) throws SinkException { + // virtual tx + String randomId = Long.toString(System.currentTimeMillis()) + RandomUtils.nextLong(); + writeTrans(buildBeginTransactionMetadata(randomId)); + sinkContextManager.writeRandomRowChangeEvent(randomId, event); + writeTrans(buildEndTransactionMetadata(event.getFullTableName(), randomId)); + } + + public void shutdown() { + dispatchExecutor.shutdown(); + timeoutScheduler.shutdown(); + } + + @Override + public void close() throws IOException { + + } + + @Override + public void flush() { + + } + + private SinkProto.TransactionMetadata buildBeginTransactionMetadata(String id) { + SinkProto.TransactionMetadata.Builder builder = SinkProto.TransactionMetadata.newBuilder(); + builder.setStatus(SinkProto.TransactionStatus.BEGIN) + .setId(id); + return builder.build(); + } + + private SinkProto.TransactionMetadata buildEndTransactionMetadata(String fullTableName, String id) { + SinkProto.TransactionMetadata.Builder builder = SinkProto.TransactionMetadata.newBuilder(); + builder.setStatus(SinkProto.TransactionStatus.END) + .setId(id) + .setEventCount(1L); + + SinkProto.DataCollection.Builder dataCollectionBuilder = SinkProto.DataCollection.newBuilder(); + dataCollectionBuilder.setDataCollection(fullTableName) + .setEventCount(1L); + builder.addDataCollections(dataCollectionBuilder); + return builder.build(); + } +} diff --git a/src/main/java/io/pixelsdb/pixels/sink/writer/retina/SinkContext.java b/src/main/java/io/pixelsdb/pixels/sink/writer/retina/SinkContext.java new file mode 100644 index 0000000..a420efe --- /dev/null +++ b/src/main/java/io/pixelsdb/pixels/sink/writer/retina/SinkContext.java @@ -0,0 +1,174 @@ +/* + * 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.writer.retina; + +import io.pixelsdb.pixels.common.transaction.TransContext; +import io.pixelsdb.pixels.core.utils.Pair; +import io.pixelsdb.pixels.sink.SinkProto; +import io.pixelsdb.pixels.sink.event.RowChangeEvent; +import io.pixelsdb.pixels.sink.metadata.TableMetadataRegistry; +import lombok.Getter; +import lombok.Setter; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.time.LocalDateTime; +import java.util.Map; +import java.util.Queue; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.locks.Condition; +import java.util.concurrent.locks.ReentrantLock; + +public class SinkContext { + private static final Logger LOGGER = LoggerFactory.getLogger(SinkContext.class); + @Getter + final ReentrantLock lock = new ReentrantLock(); + @Getter + final Condition cond = lock.newCondition(); // this cond is wait for pixels tx + + @Getter + final ReentrantLock tableCounterLock = new ReentrantLock(); + @Getter + final Condition tableCounterCond = tableCounterLock.newCondition(); + + + @Getter + final String sourceTxId; + @Getter + final AtomicInteger pendingEvents = new AtomicInteger(0); + @Getter + final CompletableFuture completionFuture = new CompletableFuture<>(); + @Getter + final TableMetadataRegistry tableMetadataRegistry = TableMetadataRegistry.Instance(); + private final Queue> recordTimes = new ConcurrentLinkedQueue<>(); + @Getter + Map tableCounters = new ConcurrentHashMap<>(); + @Getter + @Setter + Queue orphanEvent = new ConcurrentLinkedQueue<>(); + @Getter + @Setter + SinkProto.TransactionMetadata endTx; + @Getter + private TransContext pixelsTransCtx; + @Setter + @Getter + private boolean failed = false; + @Getter + @Setter + private volatile Long startTime = null; + + public SinkContext(String sourceTxId) { + this.sourceTxId = sourceTxId; + this.pixelsTransCtx = null; + setCurrStartTime(); + } + + public SinkContext(String sourceTxId, TransContext pixelsTransCtx) { + this.sourceTxId = sourceTxId; + this.pixelsTransCtx = pixelsTransCtx; + setCurrStartTime(); + } + + + void updateCounter(String table) { + updateCounter(table, 1L); + } + + public void setPixelsTransCtx(TransContext pixelsTransCtx) { + if (this.pixelsTransCtx != null) { + throw new IllegalStateException("Pixels Trans Context Already Set"); + } + this.pixelsTransCtx = pixelsTransCtx; + } + + public void recordTimestamp(String table, LocalDateTime timestamp) { + recordTimes.offer(new Pair<>(table, timestamp)); + } + + public void updateCounter(String table, long count) { + tableCounterLock.lock(); + tableCounters.compute(table, (k, v) -> + (v == null) ? count : v + count); + tableCounterCond.signalAll(); + tableCounterLock.unlock(); + } + + public boolean isCompleted() { + try { + tableCounterLock.lock(); + if (endTx == null) { + return false; + } + for (SinkProto.DataCollection dataCollection : endTx.getDataCollectionsList()) { + Long targetEventCount = tableCounters.get(dataCollection.getDataCollection()); + long target = targetEventCount == null ? 0 : targetEventCount; + LOGGER.debug("TX {}, Table {}, event count {}, tableCursors {}", endTx.getId(), dataCollection.getDataCollection(), dataCollection.getEventCount(), target); + if (dataCollection.getEventCount() > target) { + return false; + } + } + return true; + } finally { + tableCounterLock.unlock(); + } + + } + + public int getProcessedRowsNum() { + long num = 0; + try { + tableCounterLock.lock(); + for (Long counter : tableCounters.values()) { + num += counter; + } + } finally { + tableCounterLock.unlock(); + } + return (int) num; + } + + public long getTimestamp() { + if (pixelsTransCtx == null) { + throw new RuntimeException("PixelsTransCtx is NULL"); + } + return pixelsTransCtx.getTimestamp(); + } + + public void bufferOrphanedEvent(RowChangeEvent event) { + orphanEvent.add(event); + } + + public void setCurrStartTime() { + if (startTime != null) { + return; + } + + synchronized (this) { + if (startTime == null) { + startTime = System.currentTimeMillis(); + } + } + } +} diff --git a/src/main/java/io/pixelsdb/pixels/sink/writer/retina/SinkContextManager.java b/src/main/java/io/pixelsdb/pixels/sink/writer/retina/SinkContextManager.java new file mode 100644 index 0000000..13782f7 --- /dev/null +++ b/src/main/java/io/pixelsdb/pixels/sink/writer/retina/SinkContextManager.java @@ -0,0 +1,256 @@ +/* + * 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.writer.retina; + +import io.pixelsdb.pixels.common.transaction.TransContext; +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.freshness.FreshnessClient; +import io.pixelsdb.pixels.sink.util.BlockingBoundedMap; +import io.pixelsdb.pixels.sink.util.DataTransform; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.Comparator; +import java.util.Optional; +import java.util.Queue; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicBoolean; + +public class SinkContextManager { + private static final Logger LOGGER = LoggerFactory.getLogger(SinkContextManager.class); + private static final Logger BUCKET_TRACE_LOGGER = LoggerFactory.getLogger("bucket_trace"); + private static volatile SinkContextManager instance; + private final BlockingBoundedMap activeTxContexts = new BlockingBoundedMap<>(100000); + // private final ConcurrentMap activeTxContexts = new ConcurrentHashMap<>(10000); + private final TransactionProxy transactionProxy = TransactionProxy.Instance(); + private final CommitMethod commitMethod; + private final String freshnessLevel; + private final RetinaBucketDispatcher retinaBucketDispatcher; + + private SinkContextManager() { + PixelsSinkConfig config = PixelsSinkConfigFactory.getInstance(); + if (config.getCommitMethod().equals("sync")) { + this.commitMethod = CommitMethod.Sync; + } else { + this.commitMethod = CommitMethod.Async; + } + this.freshnessLevel = config.getSinkMonitorFreshnessLevel(); + this.retinaBucketDispatcher = new RetinaBucketDispatcher(); + } + + public static SinkContextManager getInstance() { + if (instance == null) { + synchronized (SinkContextManager.class) { + if (instance == null) { + instance = new SinkContextManager(); + } + } + } + return instance; + } + + protected SinkContext getActiveTxContext(RowChangeEvent event, AtomicBoolean canWrite) { + String txId = event.getTransaction().getId(); + return activeTxContexts.compute(txId, (sourceTxId, sinkContext) -> + { + if (sinkContext == null) { + LOGGER.trace("Allocate new tx {}\torder:{}", sourceTxId, event.getTransaction().getTotalOrder()); + SinkContext newSinkContext = new SinkContext(sourceTxId); + newSinkContext.bufferOrphanedEvent(event); + return newSinkContext; + } else { + try { + sinkContext.getLock().lock(); + if (sinkContext.getPixelsTransCtx() == null) { + LOGGER.trace("Buffer in tx {}\torder:{}", sourceTxId, event.getTransaction().getTotalOrder()); + canWrite.set(false); + sinkContext.bufferOrphanedEvent(event); + return sinkContext; + } + LOGGER.trace("Ready to write in tx {}\torder:{}", sourceTxId, event.getTransaction().getTotalOrder()); + canWrite.set(true); + return sinkContext; + } finally { + sinkContext.getCond().signalAll(); + sinkContext.getLock().unlock(); + } + + } + }); + } + + protected void startTransSync(String sourceTxId) { + LOGGER.trace("Start trans {}", sourceTxId); + TransContext pixelsTransContext = transactionProxy.getNewTransContext(sourceTxId); + activeTxContexts.compute( + sourceTxId, + (k, oldCtx) -> + { + if (oldCtx == null) { + LOGGER.trace("Start trans {} without buffered events", sourceTxId); + return new SinkContext(sourceTxId, pixelsTransContext); + } else { + oldCtx.getLock().lock(); + try { + if (oldCtx.getPixelsTransCtx() != null) { + LOGGER.warn("Previous tx {} has been released, maybe due to loop process", sourceTxId); + oldCtx.tableCounters = new ConcurrentHashMap<>(); + } + LOGGER.trace("Start trans with buffered events {}", sourceTxId); + oldCtx.setPixelsTransCtx(pixelsTransContext); + handleOrphanEvents(oldCtx); + oldCtx.getCond().signalAll(); + } catch (SinkException e) { + throw new RuntimeException(e); + } finally { + oldCtx.getLock().unlock(); + } + return oldCtx; + } + } + ); + LOGGER.trace("Begin Tx Sync: {}", sourceTxId); + } + + void processTxCommit(SinkProto.TransactionMetadata txEnd) { + String txId = txEnd.getId(); + SinkContext ctx = getSinkContext(txId); + if (ctx == null) { + throw new RuntimeException("Sink Context is null"); + } + + try { + ctx.tableCounterLock.lock(); + ctx.setEndTx(txEnd); + long startTs = System.currentTimeMillis(); + if (ctx.isCompleted()) { + endTransaction(ctx); + } + } finally { + ctx.tableCounterLock.unlock(); + } + } + + void endTransaction(SinkContext ctx) { + String txId = ctx.getSourceTxId(); + removeSinkContext(txId); + boolean failed = ctx.isFailed(); + if (!failed) { + LOGGER.trace("Committed transaction: {}, Pixels Trans is {}", txId, ctx.getPixelsTransCtx().getTransId()); + switch (commitMethod) { + case Sync -> { + transactionProxy.commitTransSync(ctx); + } + case Async -> { + transactionProxy.commitTransAsync(ctx); + } + } + if (freshnessLevel.equals("embed")) { + for (String table : ctx.getTableCounters().keySet()) { + String tableName = DataTransform.extractTableName(table); + FreshnessClient.getInstance().addMonitoredTable(tableName); + } + } + } else { + LOGGER.info("Abort transaction: {}", txId); + CompletableFuture.runAsync(() -> + { + transactionProxy.rollbackTrans(ctx.getPixelsTransCtx()); + }).whenComplete((v, ex) -> + { + if (ex != null) { + LOGGER.error("Rollback failed", ex); + } + }); + } + } + + private void handleOrphanEvents(SinkContext ctx) throws SinkException { + Queue buffered = ctx.getOrphanEvent(); + ctx.setOrphanEvent(null); + if (buffered != null) { + LOGGER.trace("Handle Orphan Events in {}", ctx.sourceTxId); + for (RowChangeEvent event : buffered) { + writeRowChangeEvent(ctx, event); + } + } + } + + protected void writeRowChangeEvent(SinkContext ctx, RowChangeEvent event) throws SinkException { + if (ctx != null) { + event.setTimeStamp(ctx.getTimestamp()); + } + retinaBucketDispatcher.writeRowChangeEvent(event, ctx); + } + + protected SinkContext getSinkContext(String txId) { + return activeTxContexts.get(txId); + } + + protected void removeSinkContext(String txId) { + activeTxContexts.remove(txId); + } + + protected void writeRandomRowChangeEvent(String randomId, RowChangeEvent event) throws SinkException { + writeRowChangeEvent(getSinkContext(randomId), event); + } + + public int getActiveTxnsNum() { + return activeTxContexts.size(); + } + + public String findMinActiveTx() { + Comparator customComparator = (key1, key2) -> + { + try { + String[] parts1 = key1.split("_"); + int int1 = Integer.parseInt(parts1[0]); + int loopId1 = Integer.parseInt(parts1[1]); + + String[] parts2 = key2.split("_"); + int int2 = Integer.parseInt(parts2[0]); + int loopId2 = Integer.parseInt(parts2[1]); + + int loopIdComparison = Integer.compare(loopId1, loopId2); + if (loopIdComparison != 0) { + return loopIdComparison; + } + return Integer.compare(int1, int2); + } catch (Exception e) { + System.err.println("Key format error for comparison: " + e.getMessage()); + return 0; + } + }; + + Optional min = activeTxContexts.keySet().stream().min(customComparator); + return min.orElse("None"); + } + + private enum CommitMethod { + Sync, + Async + } +} diff --git a/src/main/java/io/pixelsdb/pixels/sink/writer/retina/TableCrossTxWriter.java b/src/main/java/io/pixelsdb/pixels/sink/writer/retina/TableCrossTxWriter.java new file mode 100644 index 0000000..51c597c --- /dev/null +++ b/src/main/java/io/pixelsdb/pixels/sink/writer/retina/TableCrossTxWriter.java @@ -0,0 +1,196 @@ +/* + * 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.writer.retina; + + +import io.pixelsdb.pixels.retina.RetinaProto; +import io.pixelsdb.pixels.sink.event.RowChangeEvent; +import io.pixelsdb.pixels.sink.exception.SinkException; +import lombok.Getter; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.LinkedList; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.locks.ReentrantLock; + +/** + * @package: io.pixelsdb.pixels.sink.writer.retina + * @className: TableCrossTxWriter + * @author: AntiO2 + * @date: 2025/9/27 09:36 + */ +public class TableCrossTxWriter extends TableWriter { + protected final ReentrantLock writeLock = new ReentrantLock(); + @Getter + private final Logger LOGGER = LoggerFactory.getLogger(TableCrossTxWriter.class); + private final int flushBatchSize; + + public TableCrossTxWriter(String t, int bucketId) { + super(t, bucketId); + flushBatchSize = config.getFlushBatchSize(); + } + + /** + * Flush any buffered events for the current transaction. + */ + public void flush(List batch) { + writeLock.lock(); + try { + String txId = null; + List smallBatch = null; + List txIds = new ArrayList<>(); + List fullTableName = new ArrayList<>(); + List tableUpdateDataBuilderList = new LinkedList<>(); + List tableUpdateCount = new ArrayList<>(); + for (RowChangeEvent event : batch) { + String currTxId = event.getTransaction().getId(); + if (!currTxId.equals(txId)) { + if (smallBatch != null && !smallBatch.isEmpty()) { + RetinaProto.TableUpdateData.Builder builder = buildTableUpdateDataFromBatch(txId, smallBatch); + if (builder == null) { + continue; + } + tableUpdateDataBuilderList.add(builder); + tableUpdateCount.add(smallBatch.size()); + } + txIds.add(currTxId); + fullTableName.add(event.getFullTableName()); + txId = currTxId; + smallBatch = new LinkedList<>(); + } + smallBatch.add(event); + } + + if (smallBatch != null) { + RetinaProto.TableUpdateData.Builder builder = buildTableUpdateDataFromBatch(txId, smallBatch); + if (builder != null) { + tableUpdateDataBuilderList.add(buildTableUpdateDataFromBatch(txId, smallBatch)); + tableUpdateCount.add(smallBatch.size()); + } + } + + // flushRateLimiter.acquire(batch.size()); + long txStartTime = System.currentTimeMillis(); + +// if(freshnessLevel.equals("embed")) +// { +// long freshness_ts = txStartTime * 1000; +// FreshnessClient.getInstance().addMonitoredTable(tableName); +// DataTransform.updateTimeStamp(tableUpdateDataBuilderList, freshness_ts); +// } + + List tableUpdateData = new ArrayList<>(tableUpdateDataBuilderList.size()); + for (RetinaProto.TableUpdateData.Builder tableUpdateDataItem : tableUpdateDataBuilderList) { + tableUpdateData.add(tableUpdateDataItem.build()); + } + CompletableFuture updateRecordResponseCompletableFuture = + delegate.writeBatchAsync(batch.get(0).getSchemaName(), tableUpdateData); + + updateRecordResponseCompletableFuture.thenAccept( + resp -> + { + if (resp.getHeader().getErrorCode() != 0) { + failCtxs(txIds); + } else { + long txEndTime = System.currentTimeMillis(); + if (freshnessLevel.equals("row")) { + metricsFacade.recordFreshness(txEndTime - txStartTime); + } + updateCtxCounters(txIds, fullTableName, tableUpdateCount); + } + } + ); + } finally { + writeLock.unlock(); + } + } + + private void failCtxs(List txIds) { + for (String writeTxId : txIds) { + SinkContext sinkContext = SinkContextManager.getInstance().getSinkContext(writeTxId); + if (sinkContext != null) { + sinkContext.setFailed(true); + } + } + } + + private void updateCtxCounters(List txIds, List fullTableName, List tableUpdateCount) { + writeLock.lock(); + for (int i = 0; i < txIds.size(); i++) { + metricsFacade.recordRowEvent(tableUpdateCount.get(i)); + String writeTxId = txIds.get(i); + SinkContext sinkContext = SinkContextManager.getInstance().getSinkContext(writeTxId); + + try { + sinkContext.tableCounterLock.lock(); + sinkContext.recordTimestamp(fullTableName.get(i), LocalDateTime.now()); + sinkContext.updateCounter(fullTableName.get(i), tableUpdateCount.get(i)); + if (sinkContext.isCompleted()) { + SinkContextManager.getInstance().endTransaction(sinkContext); + } + } finally { + sinkContext.tableCounterLock.unlock(); + } + } + writeLock.unlock(); + } + + protected RetinaProto.TableUpdateData.Builder buildTableUpdateDataFromBatch(String txId, List smallBatch) { + SinkContext sinkContext = SinkContextManager.getInstance().getSinkContext(txId); + if (sinkContext == null) { + return null; + } + try { + sinkContext.getLock().lock(); + while (sinkContext.getPixelsTransCtx() == null) { + LOGGER.warn("Wait for tx to begin trans: {}", txId); // CODE SHOULD NOT REACH HERE + sinkContext.getCond().await(); + } + } catch (InterruptedException e) { + throw new RuntimeException(e); + } finally { + sinkContext.getLock().unlock(); + } + RowChangeEvent event1 = smallBatch.get(0); + + RetinaProto.TableUpdateData.Builder builder = RetinaProto.TableUpdateData.newBuilder() + .setTimestamp(sinkContext.getTimestamp()) + .setPrimaryIndexId(event1.getTableMetadata().getPrimaryIndexKeyId()) + .setTableName(tableName); + try { + for (RowChangeEvent smallEvent : smallBatch) { + addUpdateData(smallEvent, builder); + } + } catch (SinkException e) { + throw new RuntimeException("Flush failed for table " + tableName, e); + } + return builder; + } + + @Override + protected boolean needFlush() { + return buffer.size() >= flushBatchSize; + } +} diff --git a/src/main/java/io/pixelsdb/pixels/sink/writer/retina/TableSingleRecordWriter.java b/src/main/java/io/pixelsdb/pixels/sink/writer/retina/TableSingleRecordWriter.java new file mode 100644 index 0000000..773181b --- /dev/null +++ b/src/main/java/io/pixelsdb/pixels/sink/writer/retina/TableSingleRecordWriter.java @@ -0,0 +1,121 @@ +/* + * 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.writer.retina; + +import io.pixelsdb.pixels.common.transaction.TransContext; +import io.pixelsdb.pixels.retina.RetinaProto; +import io.pixelsdb.pixels.sink.event.RowChangeEvent; +import io.pixelsdb.pixels.sink.exception.SinkException; +import io.pixelsdb.pixels.sink.freshness.FreshnessClient; +import io.prometheus.client.Summary; +import lombok.Getter; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.ArrayList; +import java.util.LinkedList; +import java.util.List; +import java.util.concurrent.CompletableFuture; + +public class TableSingleRecordWriter extends TableCrossTxWriter { + @Getter + private final Logger LOGGER = LoggerFactory.getLogger(TableSingleRecordWriter.class); + private final TransactionProxy transactionProxy; + + public TableSingleRecordWriter(String t, int bucketId) { + super(t, bucketId); + this.transactionProxy = TransactionProxy.Instance(); + } + + /** + * Flush any buffered events for the current transaction. + */ + public void flush(List batch) { + TransContext pixelsTransContext = transactionProxy.getNewTransContext(tableName); + writeLock.lock(); + try { + List tableUpdateDataBuilderList = new LinkedList<>(); + for (RowChangeEvent event : batch) { + event.setTimeStamp(pixelsTransContext.getTimestamp()); + event.updateIndexKey(); + } + + RetinaProto.TableUpdateData.Builder builder = buildTableUpdateDataFromBatch(pixelsTransContext, batch); + if (builder != null) { + tableUpdateDataBuilderList.add(builder); + } + + // flushRateLimiter.acquire(batch.size()); + long txStartTime = System.currentTimeMillis(); + + List tableUpdateData = new ArrayList<>(tableUpdateDataBuilderList.size()); + for (RetinaProto.TableUpdateData.Builder tableUpdateDataItem : tableUpdateDataBuilderList) { + tableUpdateData.add(tableUpdateDataItem.build()); + } + + final Summary.Timer startWriteLatencyTimer = metricsFacade.startWriteLatencyTimer(tableName); + CompletableFuture updateRecordResponseCompletableFuture = delegate.writeBatchAsync(batch.get(0).getSchemaName(), tableUpdateData); + + updateRecordResponseCompletableFuture.thenAccept( + resp -> + { + if (freshness_embed) { + FreshnessClient.getInstance().addMonitoredTable(tableName); + } + + if (resp.getHeader().getErrorCode() != 0) { + transactionProxy.rollbackTrans(pixelsTransContext); + } else { + metricsFacade.recordRowEvent(batch.size()); + long txEndTime = System.currentTimeMillis(); + if (freshnessLevel.equals("row")) { + metricsFacade.recordFreshness(txEndTime - txStartTime); + } + transactionProxy.commitTrans(pixelsTransContext); + if (startWriteLatencyTimer != null) { + startWriteLatencyTimer.observeDuration(); + } + } + } + ); + } catch (SinkException e) { + throw new RuntimeException(e); + } finally { + writeLock.unlock(); + } + } + + protected RetinaProto.TableUpdateData.Builder buildTableUpdateDataFromBatch(TransContext transContext, List smallBatch) { + RowChangeEvent event1 = smallBatch.get(0); + RetinaProto.TableUpdateData.Builder builder = RetinaProto.TableUpdateData.newBuilder() + .setTimestamp(transContext.getTimestamp()) + .setPrimaryIndexId(event1.getTableMetadata().getPrimaryIndexKeyId()) + .setTableName(tableName); + try { + for (RowChangeEvent smallEvent : smallBatch) { + addUpdateData(smallEvent, builder); + } + } catch (SinkException e) { + throw new RuntimeException("Flush failed for table " + tableName, e); + } + return builder; + } +} diff --git a/src/main/java/io/pixelsdb/pixels/sink/writer/retina/TableSingleTxWriter.java b/src/main/java/io/pixelsdb/pixels/sink/writer/retina/TableSingleTxWriter.java new file mode 100644 index 0000000..ba61d69 --- /dev/null +++ b/src/main/java/io/pixelsdb/pixels/sink/writer/retina/TableSingleTxWriter.java @@ -0,0 +1,106 @@ +/* + * 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.writer.retina; + +import io.pixelsdb.pixels.retina.RetinaProto; +import io.pixelsdb.pixels.sink.event.RowChangeEvent; +import io.pixelsdb.pixels.sink.exception.SinkException; +import lombok.Getter; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.ArrayList; +import java.util.List; + +public class TableSingleTxWriter extends TableWriter { + private static final long TX_TIMEOUT_MS = 3000; + @Getter + private final Logger LOGGER = LoggerFactory.getLogger(TableSingleTxWriter.class); + + public TableSingleTxWriter(String tableName, int bucketId) { + super(tableName, bucketId); + } + + /** + * Flush any buffered events for the current transaction. + */ + public void flush(List batchToFlush) { + List batch; + String txId; + RetinaProto.TableUpdateData.Builder toBuild; + SinkContext sinkContext = null; + bufferLock.lock(); + try { + if (buffer.isEmpty() || currentTxId == null) { + return; + } + txId = currentTxId; + currentTxId = null; + + sinkContext = SinkContextManager.getInstance().getSinkContext(txId); + sinkContext.getLock().lock(); + try { + while (sinkContext.getPixelsTransCtx() == null) { + LOGGER.warn("Wait for prev tx to begin trans: {}", txId); + sinkContext.getCond().await(); + } + } finally { + sinkContext.getLock().unlock(); + } + + // Swap buffers quickly under lock + batch = buffer; + buffer = new ArrayList<>(); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } finally { + bufferLock.unlock(); + } + + RowChangeEvent event1 = batch.get(0); + + RetinaProto.TableUpdateData.Builder builder = RetinaProto.TableUpdateData.newBuilder() + .setPrimaryIndexId(event1.getTableMetadata().getPrimaryIndexKeyId()) + .setTableName(tableName); + + + try { + for (RowChangeEvent event : batch) { + addUpdateData(event, builder); + } + List tableUpdateData = List.of(builder.build()); + delegate.writeTrans(event1.getSchemaName(), tableUpdateData); + sinkContext.updateCounter(fullTableName, batch.size()); + // ---- Outside lock: build proto and write ---- + LOGGER.info("Flushing {} events for table {} txId={}", batch.size(), fullTableName, txId); + } catch (SinkException e) { + throw new RuntimeException("Flush failed for table " + tableName, e); + } + } + + @Override + protected boolean needFlush() { + if (currentTxId == null || !currentTxId.equals(txId)) { + return !buffer.isEmpty(); + } + return false; + } +} diff --git a/src/main/java/io/pixelsdb/pixels/sink/writer/retina/TableWriter.java b/src/main/java/io/pixelsdb/pixels/sink/writer/retina/TableWriter.java new file mode 100644 index 0000000..cbd251d --- /dev/null +++ b/src/main/java/io/pixelsdb/pixels/sink/writer/retina/TableWriter.java @@ -0,0 +1,246 @@ +/* + * 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.writer.retina; + + +import io.pixelsdb.pixels.retina.RetinaProto; +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.util.FlushRateLimiter; +import io.pixelsdb.pixels.sink.util.MetricsFacade; +import org.slf4j.Logger; + +import java.io.IOException; +import java.util.LinkedList; +import java.util.List; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.locks.Condition; +import java.util.concurrent.locks.ReentrantLock; + +/** + * @package: io.pixelsdb.pixels.sink.writer.retina + * @className: TableWriter + * @author: AntiO2 + * @date: 2025/9/27 09:58 + */ +public abstract class TableWriter { + + protected final RetinaServiceProxy delegate; // physical writer + protected final ReentrantLock bufferLock = new ReentrantLock(); + protected final Condition flushCondition = bufferLock.newCondition(); + protected final Thread flusherThread; + protected final String tableName; + protected final long flushInterval; + protected final FlushRateLimiter flushRateLimiter; + protected final SinkContextManager sinkContextManager; + protected final String freshnessLevel; + protected final boolean freshness_embed; + private final ScheduledExecutorService flushExecutor = Executors.newSingleThreadScheduledExecutor(); + private final ScheduledExecutorService logScheduler = Executors.newScheduledThreadPool(1); + private final AtomicInteger counter = new AtomicInteger(); + protected volatile boolean running = true; + // Shared state (protected by lock) + protected List buffer = new LinkedList<>(); + protected volatile String currentTxId = null; + protected String txId = null; + protected String fullTableName; + protected PixelsSinkConfig config; + protected MetricsFacade metricsFacade = MetricsFacade.getInstance(); + protected TransactionMode transactionMode; + + protected TableWriter(String tableName, int bucketId) { + this.config = PixelsSinkConfigFactory.getInstance(); + this.tableName = tableName; + this.flushInterval = config.getFlushIntervalMs(); + this.flushRateLimiter = FlushRateLimiter.getInstance(); + this.sinkContextManager = SinkContextManager.getInstance(); + this.freshnessLevel = config.getSinkMonitorFreshnessLevel(); + this.delegate = new RetinaServiceProxy(bucketId); + this.transactionMode = config.getTransactionMode(); + String sinkMonitorFreshnessLevel = config.getSinkMonitorFreshnessLevel(); + if (sinkMonitorFreshnessLevel.equals("embed")) { + freshness_embed = true; + } else { + freshness_embed = false; + } + if (this.config.isMonitorReportEnabled() && this.config.isRetinaLogQueueEnabled()) { + long interval = this.config.getMonitorReportInterval(); + Runnable monitorTask = writerInfoTask(tableName); + logScheduler.scheduleAtFixedRate( + monitorTask, + 0, + interval, + TimeUnit.MILLISECONDS + ); + } + this.flusherThread = new Thread(new FlusherRunnable(), "Pixels-Flusher-" + tableName); + this.flusherThread.start(); + } + + /** + * Helper: add insert/delete data into proto builder. + */ + protected static void addUpdateData(RowChangeEvent rowChangeEvent, + RetinaProto.TableUpdateData.Builder builder) throws SinkException { + switch (rowChangeEvent.getOp()) { + case SNAPSHOT, INSERT -> { + RetinaProto.InsertData.Builder insertDataBuilder = RetinaProto.InsertData.newBuilder(); + insertDataBuilder.addIndexKeys(rowChangeEvent.getAfterKey()); + insertDataBuilder.addAllColValues(rowChangeEvent.getAfterData()); + builder.addInsertData(insertDataBuilder); + } + case UPDATE -> { + RetinaProto.UpdateData.Builder updateDataBuilder = RetinaProto.UpdateData.newBuilder(); + updateDataBuilder.addIndexKeys(rowChangeEvent.getAfterKey()); + updateDataBuilder.addAllColValues(rowChangeEvent.getAfterData()); + builder.addUpdateData(updateDataBuilder); + } + case DELETE -> { + RetinaProto.DeleteData.Builder deleteDataBuilder = RetinaProto.DeleteData.newBuilder(); + deleteDataBuilder.addIndexKeys(rowChangeEvent.getBeforeKey()); + builder.addDeleteData(deleteDataBuilder); + } + case UNRECOGNIZED -> { + throw new SinkException("Unrecognized op: " + rowChangeEvent.getOp()); + } + } + } + + private void submitFlushTask(List batch) { + if (batch == null || batch.isEmpty()) { + return; + } + flushExecutor.submit(() -> + { + flush(batch); + }); + } + + private Runnable writerInfoTask(String tableName) { + final AtomicInteger reportId = new AtomicInteger(); + final AtomicInteger lastRunCounter = new AtomicInteger(); + Runnable monitorTask = () -> + { + String firstTx = "none"; + RowChangeEvent firstEvent = null; + int len = 0; + bufferLock.lock(); + len = buffer.size(); + if (!buffer.isEmpty()) { + firstEvent = buffer.get(0); + } + bufferLock.unlock(); + if (firstEvent != null) { + firstTx = firstEvent.getTransaction().getId(); + int count = counter.get(); + getLOGGER().info("{} Writer {}: Tx Now is {}. Buffer Len is {}. Total Count {}", reportId.incrementAndGet(), tableName, firstTx, len, count); + } + }; + return monitorTask; + } + + protected abstract Logger getLOGGER(); + + public boolean write(RowChangeEvent event, SinkContext ctx) { + try { + bufferLock.lock(); + try { + if (!transactionMode.equals(TransactionMode.RECORD)) { + txId = ctx.getSourceTxId(); + } + currentTxId = txId; + if (fullTableName == null) { + fullTableName = event.getFullTableName(); + } + counter.incrementAndGet(); + buffer.add(event); + + if (needFlush()) { + flushCondition.signalAll(); + } + } finally { + bufferLock.unlock(); + } + return true; + } catch (Exception e) { + getLOGGER().error("Write failed for table {}", tableName, e); + return false; + } + } + + public abstract void flush(List batchToFlush); + + protected abstract boolean needFlush(); + + public void close() { + this.running = false; + if (this.flusherThread != null) { + this.flusherThread.interrupt(); + } + logScheduler.shutdown(); + try { + logScheduler.awaitTermination(5, TimeUnit.SECONDS); + flushExecutor.awaitTermination(5, TimeUnit.SECONDS); + if (this.flusherThread != null) { + this.flusherThread.join(5000); + } + delegate.close(); + } catch (InterruptedException ignored) { + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + private class FlusherRunnable implements Runnable { + @Override + public void run() { + while (running) { + bufferLock.lock(); + try { + if (!needFlush()) { + try { + // Conditional wait: will wait until signaled by write() or timeout + flushCondition.await(flushInterval, TimeUnit.MILLISECONDS); + } catch (InterruptedException e) { + // Exit loop if interrupted during shutdown + running = false; + Thread.currentThread().interrupt(); + return; + } + } + + List batchToFlush = buffer; + buffer = new LinkedList<>(); + bufferLock.unlock(); + submitFlushTask(batchToFlush); + bufferLock.lock(); + } finally { + bufferLock.unlock(); + } + } + } + } +} diff --git a/src/main/java/io/pixelsdb/pixels/sink/writer/retina/TableWriterProxy.java b/src/main/java/io/pixelsdb/pixels/sink/writer/retina/TableWriterProxy.java new file mode 100644 index 0000000..3733e3c --- /dev/null +++ b/src/main/java/io/pixelsdb/pixels/sink/writer/retina/TableWriterProxy.java @@ -0,0 +1,74 @@ +/* + * 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.writer.retina; + +import io.pixelsdb.pixels.common.node.BucketCache; +import io.pixelsdb.pixels.daemon.NodeProto; +import io.pixelsdb.pixels.sink.config.PixelsSinkConfig; +import io.pixelsdb.pixels.sink.config.factory.PixelsSinkConfigFactory; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +public class TableWriterProxy { + private final static TableWriterProxy INSTANCE = new TableWriterProxy(); + + private final TransactionMode transactionMode; + private final int retinaCliNum; + private final Map WRITER_REGISTRY = new ConcurrentHashMap<>(); + + private TableWriterProxy() { + PixelsSinkConfig pixelsSinkConfig = PixelsSinkConfigFactory.getInstance(); + this.transactionMode = pixelsSinkConfig.getTransactionMode(); + this.retinaCliNum = pixelsSinkConfig.getRetinaClientNum(); + } + + protected static TableWriterProxy getInstance() { + return INSTANCE; + } + + protected TableWriter getTableWriter(String tableName, long tableId, int bucket) { + int cliNo = bucket % retinaCliNum; + // warn: we assume table id is less than INT.MAX + WriterKey key = new WriterKey(tableId, BucketCache.getInstance().getRetinaNodeInfoByBucketId(bucket), cliNo); + + return WRITER_REGISTRY.computeIfAbsent(key, t -> + { + switch (transactionMode) { + case SINGLE -> { + return new TableSingleTxWriter(tableName, bucket); + } + case BATCH -> { + return new TableCrossTxWriter(tableName, bucket); + } + case RECORD -> { + return new TableSingleRecordWriter(tableName, bucket); + } + default -> { + throw new IllegalArgumentException("Unknown transaction mode: " + transactionMode); + } + } + }); + } + + record WriterKey(long tableId, NodeProto.NodeInfo nodeInfo, int cliNo) { + } +} diff --git a/src/main/java/io/pixelsdb/pixels/sink/writer/retina/TransactionMode.java b/src/main/java/io/pixelsdb/pixels/sink/writer/retina/TransactionMode.java new file mode 100644 index 0000000..145095e --- /dev/null +++ b/src/main/java/io/pixelsdb/pixels/sink/writer/retina/TransactionMode.java @@ -0,0 +1,36 @@ +/* + * 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.writer.retina; + +public enum TransactionMode { + SINGLE, + RECORD, + BATCH; + + public static TransactionMode fromValue(String value) { + for (TransactionMode mode : values()) { + if (mode.name().equalsIgnoreCase(value)) { + return mode; + } + } + throw new RuntimeException(String.format("Can't convert %s to transaction mode", value)); + } +} diff --git a/src/main/java/io/pixelsdb/pixels/sink/writer/retina/TransactionProxy.java b/src/main/java/io/pixelsdb/pixels/sink/writer/retina/TransactionProxy.java new file mode 100644 index 0000000..d1092c7 --- /dev/null +++ b/src/main/java/io/pixelsdb/pixels/sink/writer/retina/TransactionProxy.java @@ -0,0 +1,256 @@ +/* + * 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.writer.retina; + +import io.pixelsdb.pixels.common.exception.TransException; +import io.pixelsdb.pixels.common.transaction.TransContext; +import io.pixelsdb.pixels.common.transaction.TransService; +import io.pixelsdb.pixels.sink.config.PixelsSinkConfig; +import io.pixelsdb.pixels.sink.config.factory.PixelsSinkConfigFactory; +import io.pixelsdb.pixels.sink.util.MetricsFacade; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.ArrayList; +import java.util.List; +import java.util.Queue; +import java.util.concurrent.*; +import java.util.concurrent.atomic.AtomicInteger; + +/** + * This class if for pixels trans service + * + * @author AntiO2 + */ +public class TransactionProxy { + private static final Logger LOGGER = LoggerFactory.getLogger(TransactionProxy.class); + private static volatile TransactionProxy instance; + private final TransService transService; + private final Queue transContextQueue; + private final Object batchLock = new Object(); + private final ExecutorService batchCommitExecutor; + private final MetricsFacade metricsFacade = MetricsFacade.getInstance(); + private final BlockingQueue toCommitTransContextQueue; + private final String freshnessLevel; + private final int BATCH_SIZE; + private final int WORKER_COUNT; + private final int MAX_WAIT_MS; + + private AtomicInteger beginCount = new AtomicInteger(0); + private AtomicInteger commitCount = new AtomicInteger(0); + + private TransactionProxy() { + PixelsSinkConfig pixelsSinkConfig = PixelsSinkConfigFactory.getInstance(); + BATCH_SIZE = pixelsSinkConfig.getCommitBatchSize(); + WORKER_COUNT = pixelsSinkConfig.getCommitBatchWorkers(); + MAX_WAIT_MS = pixelsSinkConfig.getCommitBatchDelay(); + + this.transService = TransService.Instance(); + this.transContextQueue = new ConcurrentLinkedDeque<>(); + this.toCommitTransContextQueue = new LinkedBlockingQueue<>(); + this.batchCommitExecutor = Executors.newFixedThreadPool( + WORKER_COUNT, + r -> + { + Thread t = new Thread(r); + t.setName("commit-trans-batch-thread"); + t.setDaemon(true); + return t; + } + ); + for (int i = 0; i < WORKER_COUNT; i++) { + batchCommitExecutor.submit(this::batchCommitWorker); + } + + this.freshnessLevel = pixelsSinkConfig.getSinkMonitorFreshnessLevel(); + } + + public static TransactionProxy Instance() { + if (instance == null) { + synchronized (TransactionProxy.class) { + if (instance == null) { + instance = new TransactionProxy(); + } + } + } + return instance; + } + + public static void staticClose() { + if (instance != null) { + instance.close(); + } + } + + private void requestTransactions() { + try { + List newContexts = transService.beginTransBatch(1000, false); + transContextQueue.addAll(newContexts); + } catch (TransException e) { + throw new RuntimeException("Batch request failed", e); + } + } + + @Deprecated + public TransContext getNewTransContext() { + return getNewTransContext("None"); + } + + public TransContext getNewTransContext(String txId) { + beginCount.incrementAndGet(); + if (true) { + try { + TransContext transContext = transService.beginTrans(false); + LOGGER.trace("{} begin {}", txId, transContext.getTransId()); + return transContext; + } catch (TransException e) { + throw null; + } + } + + TransContext ctx = transContextQueue.poll(); + if (ctx != null) { + return ctx; + } + synchronized (batchLock) { + ctx = transContextQueue.poll(); + if (ctx == null) { + requestTransactions(); + ctx = transContextQueue.poll(); + if (ctx == null) { + throw new IllegalStateException("No contexts available"); + } + } + return ctx; + } + } + + public void commitTransAsync(SinkContext transContext) { + toCommitTransContextQueue.add(transContext); + } + + public void commitTransSync(SinkContext transContext) { + commitTrans(transContext.getPixelsTransCtx()); + metricsFacade.recordTransaction(); + long txEndTime = System.currentTimeMillis(); + + if (freshnessLevel.equals("txn")) { + metricsFacade.recordFreshness(txEndTime - transContext.getStartTime()); + } + } + + public void commitTrans(TransContext ctx) { + commitCount.incrementAndGet(); + try { + transService.commitTrans(ctx.getTransId(), false); + } catch (TransException e) { + LOGGER.error("Batch commit failed: {}", e.getMessage(), e); + } + } + + public void rollbackTrans(TransContext ctx) { + try { + transService.rollbackTrans(ctx.getTransId(), false); + } catch (TransException e) { + LOGGER.error("Rollback transaction failed: {}", e.getMessage(), e); + } + } + + private void batchCommitWorker() { + List batchTransIds = new ArrayList<>(BATCH_SIZE); + List batchContexts = new ArrayList<>(BATCH_SIZE); + List txStartTimes = new ArrayList<>(BATCH_SIZE); + while (true) { + try { + batchContexts.clear(); + batchTransIds.clear(); + txStartTimes.clear(); + + SinkContext firstSinkContext = toCommitTransContextQueue.take(); + TransContext transContext = firstSinkContext.getPixelsTransCtx(); + batchContexts.add(transContext); + batchTransIds.add(transContext.getTransId()); + txStartTimes.add(firstSinkContext.getStartTime()); + long startTime = System.nanoTime(); + + while (batchContexts.size() < BATCH_SIZE) { + long elapsedMs = (System.nanoTime() - startTime) / 1_000_000; + long remainingMs = MAX_WAIT_MS - elapsedMs; + if (remainingMs <= 0) { + break; + } + + SinkContext ctx = toCommitTransContextQueue.poll(remainingMs, TimeUnit.MILLISECONDS); + if (ctx == null) { + break; + } + transContext = ctx.getPixelsTransCtx(); + batchContexts.add(transContext); + batchTransIds.add(transContext.getTransId()); + txStartTimes.add(ctx.getStartTime()); + } + + transService.commitTransBatch(batchTransIds, false); + metricsFacade.recordTransaction(batchTransIds.size()); + long txEndTime = System.currentTimeMillis(); + + if (freshnessLevel.equals("txn")) { + txStartTimes.forEach( + txStartTime -> + { + metricsFacade.recordFreshness(txEndTime - txStartTime); + } + ); + } + if (LOGGER.isTraceEnabled()) { + LOGGER.trace("[{}] Batch committed {} transactions ({} waited ms)", + Thread.currentThread().getName(), + batchTransIds.size(), + (System.nanoTime() - startTime) / 1_000_000); + } + } catch (InterruptedException ie) { + LOGGER.warn("Batch commit worker interrupted, exiting..."); + Thread.currentThread().interrupt(); + break; + } catch (TransException e) { + LOGGER.error("Batch commit failed: {}", e.getMessage(), e); + } catch (Exception e) { + LOGGER.error("Unexpected error in batch commit worker", e); + } + } + } + + public void close() { + synchronized (batchLock) { + while (true) { + TransContext ctx = transContextQueue.poll(); + if (ctx == null) { + break; + } + try { + transService.rollbackTrans(ctx.getTransId(), false); + } catch (TransException e) { + throw new RuntimeException(e); + } + } + } + } +} diff --git a/src/main/resources/log4j2.properties b/src/main/resources/log4j2.properties index e282341..f385702 100644 --- a/src/main/resources/log4j2.properties +++ b/src/main/resources/log4j2.properties @@ -1,28 +1,43 @@ -status = info -name = pixels-sink - -filter.threshold.type = ThresholdFilter -#filter.threshold.level = info -filter.threshold.level=debug - -appender.console.type = Console -appender.console.name = STDOUT -appender.console.layout.type = PatternLayout -appender.console.layout.pattern = %-d{yyyy-MM-dd HH:mm:ss,SSS} [%c]-[%p] %m%n - -appender.rolling.type = File -appender.rolling.name = log -appender.rolling.append = true -appender.rolling.fileName = ${env:PIXELS_HOME}/logs/pixels-sink.log -appender.rolling.layout.type = PatternLayout -appender.rolling.layout.pattern = %-d{yyyy-MM-dd HH:mm:ss,SSS} [%c]-[%p] %m%n - -rootLogger.level = info -rootLogger.appenderRef.stdout.ref = STDOUT -rootLogger.appenderRef.log.ref = log - -logger.transaction.name=io.pixelsdb.pixels.sink.concurrent.TransactionCoordinator +status=info +name=pixels-sink +filter.threshold.type=ThresholdFilter +filter.threshold.level=info +appender.console.type=Console +appender.console.name=STDOUT +appender.console.layout.type=PatternLayout +appender.console.layout.pattern=%-d{yyyy-MM-dd HH:mm:ss,SSS} [%c]-[%p] %m%n +appender.rolling.type=File +appender.rolling.name=log +appender.rolling.append=true +appender.rolling.fileName=${env:PIXELS_HOME}/logs/pixels-sink.log +appender.rolling.layout.type=PatternLayout +appender.rolling.layout.pattern=%-d{yyyy-MM-dd HH:mm:ss,SSS} [%c]-[%p] %m%n +rootLogger.level=info +rootLogger.appenderRef.stdout.ref=STDOUT +rootLogger.appenderRef.log.ref=log +logger.transaction.name=io.pixelsdb.pixels.sink.sink.retina.RetinaWriter logger.transaction.level=info logger.transaction.appenderRef.log.ref=log logger.transaction.appenderRef.stdout.ref=STDOUT -logger.transaction.additivity=false \ No newline at end of file +logger.transaction.additivity=false +logger.grpc.name=io.grpc.netty.shaded.io.grpc.netty.NettyClientHandler +logger.grpc.level=info +logger.grpc.additivity=false +logger.grpc.appenderRef.log.ref=log +logger.grpc.appenderRef.stdout.ref=STDOUT +log4j2.logger.io.grpc.netty.shaded.io.grpc.netty.NettyClientHandler=OFF +logger.netty-shaded.name=io.grpc.netty.shaded.io.netty +logger.netty-shaded.level=info +logger.netty-shaded.additivity=false + +appender.bucket_trace.type=File +appender.bucket_trace.name=BUCKET_TRACE +appender.bucket_trace.append=true +appender.bucket_trace.fileName=${env:PIXELS_HOME}/logs/bucket_trace.log +appender.bucket_trace.layout.type=PatternLayout +appender.bucket_trace.layout.pattern=%m%n + +logger.bucket_trace.name=bucket_trace +logger.bucket_trace.level=warn +logger.bucket_trace.additivity=false +logger.bucket_trace.appenderRef.bucket_trace.ref=BUCKET_TRACE diff --git a/src/main/resources/logging.properties b/src/main/resources/logging.properties new file mode 100644 index 0000000..2c1fd24 --- /dev/null +++ b/src/main/resources/logging.properties @@ -0,0 +1,3 @@ +.level=INFO +io.grpc.level=INFO +io.grpc.netty.level=INFO diff --git a/src/main/resources/pixels-sink.aws.properties b/src/main/resources/pixels-sink.aws.properties new file mode 100644 index 0000000..5cabc00 --- /dev/null +++ b/src/main/resources/pixels-sink.aws.properties @@ -0,0 +1,78 @@ +# engine | kafka | storage +sink.datasource=storage +# Sink Config: retina | csv | proto | none +sink.mode=none +# Kafka Config +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 +# Topic & Database Config +topic.prefix=postgresql.oltp_server +consumer.capture_database=pixels_bench_sf1x +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.retina.mode=stub +sink.remote.host=localhost +sink.remote.port=29422 +sink.timeout.ms=5000 +sink.flush.interval.ms=100 +sink.flush.batch.size=500 +sink.max.retries=3 +## writer commit +sink.commit.batch.size=500 +sink.commit.batch.worker=32 +sink.commit.batch.delay=200 +## Proto Config +sink.proto.dir=file:///home/ubuntu/pixels-sink/tmp +sink.proto.data=data +sink.proto.maxRecords=1000000 +## Schema Registry +sink.registry.url=http://localhost:8080/apis/registry/v2 +# Transaction Config +transaction.topic.suffix=transaction +#transaction.topic.value.deserializer=io.pixelsdb.pixels.sink.deserializer.TransactionAvroMessageDeserializer +transaction.topic.value.deserializer=io.pixelsdb.pixels.sink.event.deserializer.TransactionJsonMessageDeserializer +sink.trans.batch.size=200 +## Batch or trans or record +sink.trans.mode=batch +# Sink Metrics +sink.monitor.enable=true +sink.monitor.port=9464 +sink.monitor.report.interval=5000 +sink.monitor.report.file=/home/ubuntu/pixels-sink/tmp/readonly3.csv +# Interact with other rpc +sink.rpc.enable=true +sink.rpc.mock.delay=20 +# debezium +debezium.name=testEngine +debezium.connector.class=io.debezium.connector.postgresql.PostgresConnector +debezium.provide.transaction.metadata=true +debezium.offset.storage=org.apache.kafka.connect.storage.FileOffsetBackingStore +debezium.offset.storage.file.filename=/tmp/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/schemahistory.dat +debezium.database.hostname=realtime-pg-2 +debezium.database.port=5432 +debezium.database.user=pixels +debezium.database.password=pixels_realtime_crud +debezium.database.dbname=pixels_bench_sf1x +debezium.plugin.name=pgoutput +debezium.database.server.id=1 +debezium.schema.include.list=public +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=postgresql.oltp_server +debezium.transforms=topicRouting +debezium.transforms.topicRouting.type=org.apache.kafka.connect.transforms.RegexRouter +debezium.transforms.topicRouting.regex=postgresql\\.oltp_server\\.public\\.(.*) +debezium.transforms.topicRouting.replacement=postgresql.oltp_server.pixels_bench_sf1x.$1 diff --git a/src/main/resources/pixels-sink.local.properties b/src/main/resources/pixels-sink.local.properties index 1d9ed13..360cfb1 100644 --- a/src/main/resources/pixels-sink.local.properties +++ b/src/main/resources/pixels-sink.local.properties @@ -1,37 +1,78 @@ +# engine | kafka | storage +sink.datasource=storage +# Sink Config: retina | csv | proto | none +sink.mode=proto # Kafka Config bootstrap.servers=localhost:29092 -group.id=2050 +group.id=3107 auto.offset.reset=earliest key.deserializer=org.apache.kafka.common.serialization.StringDeserializer -value.deserializer=io.pixelsdb.pixels.sink.deserializer.RowChangeEventAvroDeserializer - +#value.deserializer=io.pixelsdb.pixels.sink.deserializer.RowChangeEventAvroDeserializer +value.deserializer=io.pixelsdb.pixels.sink.event.deserializer.RowChangeEventJsonDeserializer # Topic & Database Config -topic.prefix=oltp_server -consumer.capture_database=pixels_realtime_crud +topic.prefix=postgresql.oltp_server +consumer.capture_database=pixels_bench_sf1x consumer.include_tables= - -# Sink Config -sink.mode=retina sink.csv.path=./data sink.csv.enable_header=false - +## Retina Config +sink.retina.embedded=false +# stub or stream +sink.retina.mode=stream +#sink.retina.mode=stub sink.remote.host=localhost sink.remote.port=29422 sink.batch.size=100 sink.timeout.ms=5000 -sink.flush.interval.ms=5000 +sink.flush.interval.ms=100 +sink.flush.batch.size=100 sink.max.retries=3 - +## sink commit +sink.commit.batch.size=500 +sink.commit.batch.worker=16 +sink.commit.batch.delay=200 +## Proto Config +sink.proto.dir=file:///home/pixels/projects/pixels-sink/tmp +sink.proto.data=data +sink.proto.maxRecords=1000000 ## Schema Registry sink.registry.url=http://localhost:8080/apis/registry/v2 - # Transaction Config transaction.topic.suffix=transaction -transaction.topic.value.deserializer=io.pixelsdb.pixels.sink.deserializer.TransactionAvroMessageDeserializer +#transaction.topic.value.deserializer=io.pixelsdb.pixels.sink.deserializer.TransactionAvroMessageDeserializer +transaction.topic.value.deserializer=io.pixelsdb.pixels.sink.event.deserializer.TransactionJsonMessageDeserializer sink.trans.batch.size=100 +## Batch or trans or record +sink.trans.mode=batch # Sink Metrics sink.monitor.enable=true sink.monitor.port=9464 # Interact with other rpc -sink.rpc.enable=false -sink.rpc.mock.delay=20 \ No newline at end of file +sink.rpc.enable=true +sink.rpc.mock.delay=20 +# debezium +debezium.name=testEngine +debezium.connector.class=io.debezium.connector.postgresql.PostgresConnector +debezium.provide.transaction.metadata=true +debezium.offset.storage=org.apache.kafka.connect.storage.FileOffsetBackingStore +debezium.offset.storage.file.filename=/tmp/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/schemahistory.dat +debezium.database.hostname=localhost +debezium.database.port=5432 +debezium.database.user=pixels +debezium.database.password=pixels_realtime_crud +debezium.database.dbname=pixels_bench_sf1x +debezium.plugin.name=pgoutput +debezium.database.server.id=1 +debezium.schema.include.list=public +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=postgresql.oltp_server +debezium.transforms=topicRouting +debezium.transforms.topicRouting.type=org.apache.kafka.connect.transforms.RegexRouter +debezium.transforms.topicRouting.regex=postgresql\\.oltp_server\\.public\\.(.*) +debezium.transforms.topicRouting.replacement=postgresql.oltp_server.pixels_bench_sf1x.$1 + diff --git a/src/main/resources/pixels-sink.properties b/src/main/resources/pixels-sink.properties index a766bfe..41af3ec 100644 --- a/src/main/resources/pixels-sink.properties +++ b/src/main/resources/pixels-sink.properties @@ -3,14 +3,12 @@ bootstrap.servers=pixels_kafka:9092 group.id=docker-pixels-table auto.offset.reset=earliest key.deserializer=org.apache.kafka.common.serialization.StringDeserializer -value.deserializer=io.pixelsdb.pixels.sink.deserializer.RowChangeEventAvroDeserializer -#value.deserializer=io.pixelsdb.pixels.sink.deserializer.RowChangeEventJsonDeserializer - +value.deserializer=io.pixelsdb.pixels.sink.event.deserializer.RowChangeEventAvroDeserializer +#value.deserializer=io.pixelsdb.pixels.writer.deserializer.RowChangeEventJsonDeserializer # Topic & Database Config topic.prefix=oltp_server consumer.capture_database=pixels_realtime_crud consumer.include_tables= - # Sink Config sink.mode=retina sink.csv.path=./data @@ -21,14 +19,15 @@ sink.batch.size=100 sink.timeout.ms=5000 sink.flush.interval.ms=5000 sink.max.retries=3 - ## Schema Registry sink.registry.url=http://apicurio:8080/apis/registry/v2 # Transaction Config transaction.topic.suffix=transaction -transaction.topic.value.deserializer=io.pixelsdb.pixels.sink.deserializer.TransactionAvroMessageDeserializer -#transaction.topic.value.deserializer=io.pixelsdb.pixels.sink.deserializer.TransactionJsonMessageDeserializer +transaction.topic.value.deserializer=io.pixelsdb.pixels.sink.event.deserializer.TransactionAvroMessageDeserializer +#transaction.topic.value.deserializer=io.pixelsdb.pixels.writer.deserializer.TransactionJsonMessageDeserializer sink.trans.batch.size=100 +## batch or record +sink.trans.mode=batch # Sink Metrics sink.monitor.enable=true sink.monitor.port=9464 diff --git a/src/test/java/io/pixelsdb/pixels/sink/DebeziumEngineTest.java b/src/test/java/io/pixelsdb/pixels/sink/DebeziumEngineTest.java new file mode 100644 index 0000000..58659ff --- /dev/null +++ b/src/test/java/io/pixelsdb/pixels/sink/DebeziumEngineTest.java @@ -0,0 +1,99 @@ +/* + * 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; + + +import io.debezium.embedded.Connect; +import io.debezium.engine.DebeziumEngine; +import io.debezium.engine.RecordChangeEvent; +import io.debezium.engine.format.ChangeEventFormat; +import org.apache.kafka.connect.source.SourceRecord; +import org.junit.jupiter.api.Test; + +import java.util.List; +import java.util.Properties; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +/** + * @package: io.pixelsdb.pixels.sink + * @className: DebeziumEngineTest + * @author: AntiO2 + * @date: 2025/9/25 12:16 + */ +public class DebeziumEngineTest +{ + @Test + public void testPostgresCDC() + { + final Properties props = new Properties(); + + props.setProperty("name", "testEngine"); + props.setProperty("connector.class", "io.debezium.connector.postgresql.PostgresConnector"); + props.setProperty("provide.transaction.metadata", "true"); + + props.setProperty("offset.storage", "org.apache.kafka.connect.storage.FileOffsetBackingStore"); + props.setProperty("offset.storage.file.filename", "/tmp/offsets.dat"); + props.setProperty("offset.flush.interval.ms", "60000"); + + props.setProperty("schema.history.internal", "io.debezium.storage.file.history.FileSchemaHistory"); + props.setProperty("schema.history.internal.file.filename", "/tmp/schemahistory.dat"); + + props.setProperty("database.hostname", "localhost"); + props.setProperty("database.port", "5432"); + props.setProperty("database.user", "pixels"); + props.setProperty("database.password", "pixels_realtime_crud"); + props.setProperty("database.dbname", "pixels_bench_sf1x"); + props.setProperty("plugin.name", "pgoutput"); + props.setProperty("database.server.id", "1"); + props.setProperty("schema.include.list", "public"); + props.setProperty("snapshot.mode", "never"); + + props.setProperty("key.converter", "org.apache.kafka.connect.json.JsonConverter"); + props.setProperty("value.converter", "org.apache.kafka.connect.json.JsonConverter"); + props.setProperty("topic.prefix", "postgres.cdc"); + + props.setProperty("transforms", "topicRouting"); + props.setProperty("transforms.topicRouting.type", "org.apache.kafka.connect.transforms.RegexRouter"); + props.setProperty("transforms.topicRouting.regex", "postgresql\\.oltp_server\\.public\\.(.*)"); + props.setProperty("transforms.topicRouting.replacement", "postgresql.oltp_server.pixels_bench_sf1x.$1"); + + DebeziumEngine> engine = DebeziumEngine.create(ChangeEventFormat.of(Connect.class)) + .using(props) + .notifying(new MyChangeConsumer()) + .build(); + + ExecutorService executor = Executors.newSingleThreadExecutor(); + executor.execute(engine); + + while (true) + { + + } + } + + class MyChangeConsumer implements DebeziumEngine.ChangeConsumer> + { + public void handleBatch(List> event, DebeziumEngine.RecordCommitter> committer) throws InterruptedException + { + committer.markBatchFinished(); + } + } + +} diff --git a/src/test/java/io/pixelsdb/pixels/sink/TestUtils.java b/src/test/java/io/pixelsdb/pixels/sink/TestUtils.java index b3bd4eb..7713401 100644 --- a/src/test/java/io/pixelsdb/pixels/sink/TestUtils.java +++ b/src/test/java/io/pixelsdb/pixels/sink/TestUtils.java @@ -20,9 +20,12 @@ import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; -public class TestUtils { - public static ExecutorService synchronousExecutor() { - return Executors.newSingleThreadExecutor(runnable -> { +public class TestUtils +{ + public static ExecutorService synchronousExecutor() + { + return Executors.newSingleThreadExecutor(runnable -> + { Thread thread = new Thread(runnable); thread.setDaemon(true); return thread; diff --git a/src/test/java/io/pixelsdb/pixels/sink/concurrent/TransactionCoordinatorTest.java b/src/test/java/io/pixelsdb/pixels/sink/concurrent/RetinaWriterTest.java similarity index 56% rename from src/test/java/io/pixelsdb/pixels/sink/concurrent/TransactionCoordinatorTest.java rename to src/test/java/io/pixelsdb/pixels/sink/concurrent/RetinaWriterTest.java index 4459cc6..74b9e4f 100644 --- a/src/test/java/io/pixelsdb/pixels/sink/concurrent/TransactionCoordinatorTest.java +++ b/src/test/java/io/pixelsdb/pixels/sink/concurrent/RetinaWriterTest.java @@ -21,6 +21,8 @@ import io.pixelsdb.pixels.sink.TestUtils; 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.writer.retina.RetinaWriter; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; @@ -41,47 +43,54 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertTrue; -class TransactionCoordinatorTest { - private TransactionCoordinator coordinator; +class RetinaWriterTest +{ + private static final Logger LOGGER = LoggerFactory.getLogger(RetinaWriterTest.class); + private RetinaWriter coordinator; private List dispatchedEvents; private ExecutorService testExecutor; private CountDownLatch latch; - private static final Logger LOGGER = LoggerFactory.getLogger(TransactionCoordinatorTest.class); + @BeforeEach - void setUp() throws IOException { + void setUp() throws IOException + { PixelsSinkConfigFactory.initialize(""); testExecutor = TestUtils.synchronousExecutor(); dispatchedEvents = Collections.synchronizedList(new ArrayList<>()); - coordinator = new TestableCoordinator(dispatchedEvents); + coordinator = new RetinaWriter(); - try { - Field executorField = TransactionCoordinator.class + try + { + Field executorField = RetinaWriter.class .getDeclaredField("dispatchExecutor"); executorField.setAccessible(true); executorField.set(coordinator, testExecutor); - } catch (Exception e) { + } catch (Exception e) + { throw new RuntimeException("Failed to inject executor", e); } } - private SinkProto.TransactionMetadata buildBeginTx(String txId) { + private SinkProto.TransactionMetadata buildBeginTx(String txId) + { return SinkProto.TransactionMetadata.newBuilder() .setId(txId) .setStatus(SinkProto.TransactionStatus.BEGIN) .build(); } - private SinkProto.TransactionMetadata buildEndTx(String txId) { + private SinkProto.TransactionMetadata buildEndTx(String txId) + { return SinkProto.TransactionMetadata.newBuilder() .setId(txId) .setStatus(SinkProto.TransactionStatus.END) .build(); } - private RowChangeEvent buildEvent(String txId, String table, long collectionOrder, long totalOrder) { + private RowChangeEvent buildEvent(String txId, String table, long collectionOrder, long totalOrder) throws SinkException + { return new RowChangeEvent( - SinkProto.RowRecord.newBuilder().setTransaction( SinkProto.TransactionInfo.newBuilder() .setId(txId) @@ -90,21 +99,22 @@ private RowChangeEvent buildEvent(String txId, String table, long collectionOrde .build() ).setSource( SinkProto.SourceInfo.newBuilder() - .setTable(table) - .setDb("test_db") - .build() + .setTable(table) + .setDb("test_db") + .build() ).setOp(SinkProto.OperationType.INSERT) - .build() + .build(), null ); } @Test - void shouldProcessOrderedEvents() throws Exception { - coordinator.processTransactionEvent(buildBeginTx("tx1")); + void shouldProcessOrderedEvents() throws Exception + { + coordinator.writeTrans(buildBeginTx("tx1")); - coordinator.processRowEvent(buildEvent("tx1", "orders", 1, 1)); - coordinator.processRowEvent(buildEvent("tx1", "orders", 2, 2)); - coordinator.processTransactionEvent(buildEndTx("tx1")); + coordinator.writeRow(buildEvent("tx1", "orders", 1, 1)); + coordinator.writeRow(buildEvent("tx1", "orders", 2, 2)); + coordinator.writeTrans(buildEndTx("tx1")); assertEquals(2, dispatchedEvents.size()); assertTrue(dispatchedEvents.get(0).contains("Order: 1/1")); @@ -112,71 +122,71 @@ void shouldProcessOrderedEvents() throws Exception { } @Test - void shouldHandleOutOfOrderEvents() { - coordinator.processTransactionEvent(buildBeginTx("tx2")); - coordinator.processRowEvent(buildEvent("tx2", "users", 3, 3)); - coordinator.processRowEvent(buildEvent("tx2", "users", 2, 2)); - coordinator.processRowEvent(buildEvent("tx2", "users", 1, 1)); - coordinator.processTransactionEvent(buildEndTx("tx2")); + void shouldHandleOutOfOrderEvents() throws SinkException + { + coordinator.writeTrans(buildBeginTx("tx2")); + coordinator.writeRow(buildEvent("tx2", "users", 3, 3)); + coordinator.writeRow(buildEvent("tx2", "users", 2, 2)); + coordinator.writeRow(buildEvent("tx2", "users", 1, 1)); + coordinator.writeTrans(buildEndTx("tx2")); assertTrue(dispatchedEvents.get(0).contains("Order: 1/1")); assertTrue(dispatchedEvents.get(1).contains("Order: 2/2")); assertTrue(dispatchedEvents.get(2).contains("Order: 3/3")); } @Test - void shouldRecoverOrphanedEvents() { - coordinator.processRowEvent(buildEvent("tx3", "logs", 1, 1)); // orphan event - coordinator.processTransactionEvent(buildBeginTx("tx3")); // recover - coordinator.processTransactionEvent(buildEndTx("tx3")); + void shouldRecoverOrphanedEvents() throws SinkException + { + coordinator.writeRow(buildEvent("tx3", "logs", 1, 1)); // orphan event + coordinator.writeTrans(buildBeginTx("tx3")); // recover + coordinator.writeTrans(buildEndTx("tx3")); assertTrue(dispatchedEvents.get(0).contains("Order: 1/1")); } @ParameterizedTest @EnumSource(value = SinkProto.OperationType.class, names = {"INSERT", "UPDATE", "DELETE", "SNAPSHOT"}) - void shouldProcessNonTransactionalEvents(SinkProto.OperationType opType) throws InterruptedException { + void shouldProcessNonTransactionalEvents(SinkProto.OperationType opType) throws InterruptedException, SinkException + { RowChangeEvent event = new RowChangeEvent( SinkProto.RowRecord.newBuilder() .setOp(opType) - .build() + .build(), null ); - coordinator.processRowEvent(event); + coordinator.writeRow(event); TimeUnit.MILLISECONDS.sleep(10); assertEquals(1, dispatchedEvents.size()); PixelsSinkConfigFactory.reset(); } - @Test - void shouldHandleTransactionTimeout() throws Exception { - TransactionCoordinator fastTimeoutCoordinator = new TransactionCoordinator(); - fastTimeoutCoordinator.setTxTimeoutMs(100); - - fastTimeoutCoordinator.processTransactionEvent(buildBeginTx("tx4")); - fastTimeoutCoordinator.processRowEvent(buildEvent("tx4", "temp", 1, 1)); - - TimeUnit.MILLISECONDS.sleep(150); // wait for timeout - assertEquals(1, fastTimeoutCoordinator.activeTxContexts.size()); - } - @ParameterizedTest @ValueSource(ints = {1, 3, 9, 16}) - void shouldHandleConcurrentEvents(int threadCount) throws Exception { + void shouldHandleConcurrentEvents(int threadCount) throws SinkException, IOException, InterruptedException + { PixelsSinkConfigFactory.reset(); PixelsSinkConfigFactory.initialize(""); latch = new CountDownLatch(threadCount); - coordinator.processTransactionEvent(buildBeginTx("tx5")); + coordinator.writeTrans(buildBeginTx("tx5")); // concurrently send event - for (int i = 0; i < threadCount; i++) { + for (int i = 0; i < threadCount; i++) + { int order = i + 1; - new Thread(() -> { - coordinator.processRowEvent(buildEvent("tx5", "concurrent", order, order)); + new Thread(() -> + { + try + { + coordinator.writeRow(buildEvent("tx5", "concurrent", order, order)); + } catch (SinkException e) + { + throw new RuntimeException(e); + } latch.countDown(); }).start(); } assertTrue(latch.await(1, TimeUnit.SECONDS)); - coordinator.processTransactionEvent(buildEndTx("tx5")); + coordinator.writeTrans(buildEndTx("tx5")); LOGGER.debug("Thread Count: {} DispatchedEvents size: {}", threadCount, dispatchedEvents.size()); LOGGER.debug("Thread Count: {} DispatchedEvents size: {}", threadCount, dispatchedEvents.size()); @@ -184,38 +194,4 @@ void shouldHandleConcurrentEvents(int threadCount) throws Exception { assertEquals(threadCount, dispatchedEvents.size()); PixelsSinkConfigFactory.reset(); } - - - private static class TestableCoordinator extends TransactionCoordinator { - private final List eventLog; - private static final Logger LOGGER = LoggerFactory.getLogger(TestableCoordinator.class); - TestableCoordinator(List eventLog) { - this.eventLog = eventLog; - } - - @Override - protected void dispatchImmediately(RowChangeEvent event, SinkContext ctx) { - dispatchExecutor.execute(() -> { - try { - String log = String.format("Dispatching [%s] %s.%s (Order: %s/%s)", - event.getOp().name(), - event.getDb(), - event.getTable(), - event.getTransaction() != null ? - event.getTransaction().getDataCollectionOrder() : "N/A", - event.getTransaction() != null ? - event.getTransaction().getTotalOrder() : "N/A"); - LOGGER.info(log); - eventLog.add(log); - LOGGER.debug("Event log size : {}", eventLog.size()); - } finally { - if (ctx != null) { - if (ctx.pendingEvents.decrementAndGet() == 0 && ctx.completed) { - ctx.completionFuture.complete(null); - } - } - } - }); - } - } } \ No newline at end of file diff --git a/src/test/java/io/pixelsdb/pixels/sink/concurrent/TransactionServiceTest.java b/src/test/java/io/pixelsdb/pixels/sink/concurrent/TransactionServiceTest.java index 6d710e7..0f590f1 100644 --- a/src/test/java/io/pixelsdb/pixels/sink/concurrent/TransactionServiceTest.java +++ b/src/test/java/io/pixelsdb/pixels/sink/concurrent/TransactionServiceTest.java @@ -1,52 +1,109 @@ +/* + * 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.concurrent; import io.pixelsdb.pixels.common.exception.TransException; import io.pixelsdb.pixels.common.transaction.TransContext; import io.pixelsdb.pixels.common.transaction.TransService; +import lombok.extern.slf4j.Slf4j; import org.junit.jupiter.api.Test; -import static org.junit.jupiter.api.Assertions.*; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + import java.util.List; -public class TransactionServiceTest { +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +@Slf4j +public class TransactionServiceTest +{ + private static final Logger logger = LoggerFactory.getLogger(TransactionServiceTest.class); @Test - public void testTransactionService() { + public void testTransactionService() + { int numTransactions = 10; TransService transService = TransService.CreateInstance("localhost", 18889); - try { - List transContexts = transService.beginTransBatch(numTransactions, false); + try + { + List transContexts = transService.beginTransBatch(numTransactions, false); assertEquals(numTransactions, transContexts.size()); TransContext prevTransContext = transContexts.get(0); - for(int i = 1; i < numTransactions; i++) { + for (int i = 1; i < numTransactions; i++) + { TransContext transContext = transContexts.get(i); assertTrue(transContext.getTransId() > prevTransContext.getTransId()); assertTrue(transContext.getTimestamp() > prevTransContext.getTimestamp()); prevTransContext = transContext; } - } catch (TransException e) { + } catch (TransException e) + { throw new RuntimeException(e); } } @Test - public void testBatchRequest() { + public void testBatchRequest() + { int numTransactions = 1000; TransService transService = TransService.CreateInstance("localhost", 18889); - try { - List transContexts = transService.beginTransBatch(numTransactions, false); + try + { + List transContexts = transService.beginTransBatch(numTransactions, false); assertEquals(numTransactions, transContexts.size()); TransContext prevTransContext = transContexts.get(0); - for(int i = 1; i < numTransactions; i++) { + for (int i = 1; i < numTransactions; i++) + { TransContext transContext = transContexts.get(i); assertTrue(transContext.getTransId() > prevTransContext.getTransId()); assertTrue(transContext.getTimestamp() > prevTransContext.getTimestamp()); prevTransContext = transContext; } - } catch (TransException e) { + } catch (TransException e) + { throw new RuntimeException(e); } } + + @Test + public void testAbort() throws TransException + { + TransService transService = TransService.Instance(); + TransContext transContext = transService.beginTrans(true); + + logger.info("ID {}, TS {}", transContext.getTransId(), transContext.getTimestamp()); + TransContext transContext1 = transService.beginTrans(false); + TransContext transContext2 = transService.beginTrans(false); + + logger.info("ID {}, TS {}", transContext1.getTransId(), transContext1.getTimestamp()); + logger.info("ID {}, TS {}", transContext2.getTransId(), transContext2.getTimestamp()); + transService.commitTrans(transContext2.getTransId(), false); + + transContext = transService.beginTrans(true); + logger.info("ID {}, TS {}", transContext.getTransId(), transContext.getTimestamp()); + + + } } diff --git a/src/test/java/io/pixelsdb/pixels/sink/consumer/AvroConsumerTest.java b/src/test/java/io/pixelsdb/pixels/sink/consumer/AvroConsumerTest.java index 05a2554..3ee867d 100644 --- a/src/test/java/io/pixelsdb/pixels/sink/consumer/AvroConsumerTest.java +++ b/src/test/java/io/pixelsdb/pixels/sink/consumer/AvroConsumerTest.java @@ -21,11 +21,11 @@ import io.apicurio.registry.rest.client.RegistryClientFactory; import io.apicurio.registry.serde.SerdeConfig; import io.apicurio.registry.serde.avro.AvroKafkaDeserializer; -import io.pixelsdb.pixels.retina.RetinaProto; import io.pixelsdb.pixels.sink.SinkProto; import io.pixelsdb.pixels.sink.config.factory.PixelsSinkConfigFactory; -import io.pixelsdb.pixels.sink.deserializer.RowChangeEventAvroDeserializer; import io.pixelsdb.pixels.sink.event.RowChangeEvent; +import io.pixelsdb.pixels.sink.event.deserializer.RowChangeEventAvroDeserializer; +import io.pixelsdb.pixels.sink.exception.SinkException; import org.apache.avro.Schema; import org.apache.avro.generic.GenericRecord; import org.apache.kafka.clients.consumer.ConsumerConfig; @@ -40,14 +40,16 @@ import java.util.Collections; import java.util.Properties; -public class AvroConsumerTest { +public class AvroConsumerTest +{ private static final String TOPIC = "oltp_server.pixels_realtime_crud.customer"; private static final String REGISTRY_URL = "http://localhost:8080/apis/registry/v2"; private static final String BOOTSTRAP_SERVERS = "localhost:29092"; private static final String GROUP_ID = "avro-consumer-test-group-1"; - private static KafkaConsumer getRowChangeEventAvroKafkaConsumer() { + private static KafkaConsumer getRowChangeEventAvroKafkaConsumer() + { Properties props = new Properties(); props.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, BOOTSTRAP_SERVERS); props.put(ConsumerConfig.GROUP_ID_CONFIG, GROUP_ID); @@ -62,15 +64,17 @@ private static KafkaConsumer getRowChangeEventAvroKafkaC return consumer; } - private static void processRecord(RowChangeEvent event) { - RetinaProto.RowValue.Builder builder = RetinaProto.RowValue.newBuilder(); - for (SinkProto.ColumnValue value : event.getRowRecord().getAfter().getValuesList()) { - builder.addValues(value.getValue()); - } - builder.build(); + private static void processRecord(RowChangeEvent event) + { +// RetinaProto.RowValue.Builder builder = RetinaProto.RowValue.newBuilder(); +// for (SinkProto.ColumnValue value : event.getRowRecord().getAfter().getValuesList()) { +// builder.addValues(value.getValue()); +// } +// builder.build(); } - private static KafkaConsumer getStringGenericRecordKafkaConsumer() { + private static KafkaConsumer getStringGenericRecordKafkaConsumer() + { Properties props = new Properties(); props.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, BOOTSTRAP_SERVERS); props.put(ConsumerConfig.GROUP_ID_CONFIG, GROUP_ID); @@ -85,30 +89,15 @@ private static KafkaConsumer getStringGenericRecordKafkaC return consumer; } - private static RowChangeEvent convertToRowChangeEvent(GenericRecord record, Schema schema) { - return new RowChangeEvent(SinkProto.RowRecord.newBuilder().build()); + private static RowChangeEvent convertToRowChangeEvent(GenericRecord record, Schema schema) throws SinkException + { + return new RowChangeEvent(SinkProto.RowRecord.newBuilder().build(), null); } - @Test - public void avroConsumerTest() { - KafkaConsumer consumer = getStringGenericRecordKafkaConsumer(); - consumer.subscribe(Collections.singletonList(TOPIC)); - - RegistryClient registryClient = RegistryClientFactory.create(REGISTRY_URL); - - try { - while (true) { - ConsumerRecords records = consumer.poll(Duration.ofMillis(100)); - for (ConsumerRecord record : records) { - processRecord(record, registryClient); - } - } - } finally { - consumer.close(); - } - } - private static void processRecord(ConsumerRecord record, RegistryClient registryClient) { - try { + private static void processRecord(ConsumerRecord record, RegistryClient registryClient) + { + try + { GenericRecord avroRecord = record.value(); Schema schema = avroRecord.getSchema(); @@ -123,35 +112,68 @@ private static void processRecord(ConsumerRecord record, System.out.println("Offset: " + record.offset()); System.out.println("Event: " + event); - } catch (Exception e) { + } catch (Exception e) + { System.err.println("Error processing message: " + e.getMessage()); e.printStackTrace(); } } - private static String getSchemaIdFromRegistry(RegistryClient client, Schema schema) { + private static String getSchemaIdFromRegistry(RegistryClient client, Schema schema) + { String schemaContent = schema.toString(); - try { + try + { return ""; - } catch (Exception e) { + } catch (Exception e) + { throw new RuntimeException("Schema not found in registry: " + schema.getFullName(), e); } } @Test - public void sinkConsumerTest() throws IOException { + public void avroConsumerTest() + { + KafkaConsumer consumer = getStringGenericRecordKafkaConsumer(); + consumer.subscribe(Collections.singletonList(TOPIC)); + + RegistryClient registryClient = RegistryClientFactory.create(REGISTRY_URL); + + try + { + while (true) + { + ConsumerRecords records = consumer.poll(Duration.ofMillis(100)); + for (ConsumerRecord record : records) + { + processRecord(record, registryClient); + } + } + } finally + { + consumer.close(); + } + } + + @Test + public void sinkConsumerTest() throws IOException + { PixelsSinkConfigFactory.initialize("/home/anti/work/pixels-sink/src/main/resources/pixels-sink.local.properties"); KafkaConsumer consumer = getRowChangeEventAvroKafkaConsumer(); consumer.subscribe(Collections.singletonList(TOPIC)); - try { - while (true) { + try + { + while (true) + { ConsumerRecords records = consumer.poll(Duration.ofMillis(100)); - for (ConsumerRecord record : records) { + for (ConsumerRecord record : records) + { processRecord(record.value()); } } - } finally { + } finally + { consumer.close(); } } diff --git a/src/test/java/io/pixelsdb/pixels/sink/deserializer/RowDataParserTest.java b/src/test/java/io/pixelsdb/pixels/sink/deserializer/RowDataParserTest.java deleted file mode 100644 index 13d0955..0000000 --- a/src/test/java/io/pixelsdb/pixels/sink/deserializer/RowDataParserTest.java +++ /dev/null @@ -1,47 +0,0 @@ -/* - * 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.deserializer; - -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.node.TextNode; -import io.pixelsdb.pixels.core.TypeDescription; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.CsvSource; - -import java.math.BigDecimal; - -import static org.junit.jupiter.api.Assertions.assertEquals; - -public class RowDataParserTest { - // BqQ= 17 - // JbR7 24710.35 - @ParameterizedTest - @CsvSource({ - // encodedValue, expectedValue, precision, scale - "BqQ=, 17.00, 15, 2", - "JbR7, 24710.35, 15, 2", - }) - void testParseDecimalValid(String encodedValue, String expectedValue, int precision, int scale) { - JsonNode node = new TextNode(encodedValue); - TypeDescription type = TypeDescription.createDecimal(precision, scale); - RowDataParser rowDataParser = new RowDataParser(type); - BigDecimal result = rowDataParser.parseDecimal(node, type); - assertEquals(new BigDecimal(expectedValue), result); - } - -} diff --git a/src/test/java/io/pixelsdb/pixels/sink/event/RowChangeEventTest.java b/src/test/java/io/pixelsdb/pixels/sink/event/RowChangeEventTest.java new file mode 100644 index 0000000..5ae043d --- /dev/null +++ b/src/test/java/io/pixelsdb/pixels/sink/event/RowChangeEventTest.java @@ -0,0 +1,63 @@ +/* + * 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; + +import com.google.protobuf.ByteString; +import io.pixelsdb.pixels.index.IndexProto; +import io.pixelsdb.pixels.sink.config.factory.PixelsSinkConfigFactory; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.nio.ByteBuffer; + +public class RowChangeEventTest +{ + private static Logger LOGGER = LoggerFactory.getLogger(RowChangeEventTest.class); + + @BeforeAll + public static void init() throws IOException + { + PixelsSinkConfigFactory.initialize("/home/ubuntu/pixels-sink/conf/pixels-sink.aws.properties"); + } + + + @Test + public void testSameHash() + { + for(int i = 0; i < 10; ++i) + { + ByteString indexKey = getIndexKey(0); + int bucket = RowChangeEvent.getBucketIdFromByteBuffer(indexKey); + LOGGER.info("Bucket: {}", bucket); + } + } + + private ByteString getIndexKey(int key) + { + int keySize = Integer.BYTES; + ByteBuffer byteBuffer = ByteBuffer.allocate(keySize); + byteBuffer.putInt(key); + return ByteString.copyFrom(byteBuffer.rewind()); + } +} diff --git a/src/test/java/io/pixelsdb/pixels/sink/event/deserializer/RowBatchTest.java b/src/test/java/io/pixelsdb/pixels/sink/event/deserializer/RowBatchTest.java new file mode 100644 index 0000000..0c36b72 --- /dev/null +++ b/src/test/java/io/pixelsdb/pixels/sink/event/deserializer/RowBatchTest.java @@ -0,0 +1,63 @@ +/* + * 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.core.vector.BinaryColumnVector; +import io.pixelsdb.pixels.core.vector.VectorizedRowBatch; +import org.junit.jupiter.api.Test; + +import java.util.ArrayList; +import java.util.List; + +public class RowBatchTest +{ + private final List columnNames = new ArrayList<>(); + private final List columnTypes = new ArrayList<>(); + + @Test + public void integerRowBatchTest() + { + columnNames.add("id"); + columnTypes.add("int"); + + TypeDescription schema = TypeDescription.createSchemaFromStrings(columnNames, columnTypes); + VectorizedRowBatch rowBatch = schema.createRowBatch(3, TypeDescription.Mode.CREATE_INT_VECTOR_FOR_INT); + VectorizedRowBatch newRowBatch = VectorizedRowBatch.deserialize(rowBatch.serialize()); + + } + + @Test + public void varcharRowBatchTest() + { + columnNames.add("name"); + columnTypes.add("varchar(100)"); + + TypeDescription schema = TypeDescription.createSchemaFromStrings(columnNames, columnTypes); + VectorizedRowBatch rowBatch = schema.createRowBatch(3, TypeDescription.Mode.CREATE_INT_VECTOR_FOR_INT); + BinaryColumnVector v = (BinaryColumnVector) rowBatch.cols[0]; + v.add("rr"); + v.add("zz"); + v.add("rr"); + VectorizedRowBatch newRowBatch = VectorizedRowBatch.deserialize(rowBatch.serialize()); + + } +} diff --git a/src/test/java/io/pixelsdb/pixels/sink/deserializer/RowChangeEventDeserializerTest.java b/src/test/java/io/pixelsdb/pixels/sink/event/deserializer/RowChangeEventDeserializerTest.java similarity index 91% rename from src/test/java/io/pixelsdb/pixels/sink/deserializer/RowChangeEventDeserializerTest.java rename to src/test/java/io/pixelsdb/pixels/sink/event/deserializer/RowChangeEventDeserializerTest.java index e85bbb1..783bbcb 100644 --- a/src/test/java/io/pixelsdb/pixels/sink/deserializer/RowChangeEventDeserializerTest.java +++ b/src/test/java/io/pixelsdb/pixels/sink/event/deserializer/RowChangeEventDeserializerTest.java @@ -15,7 +15,7 @@ * */ -package io.pixelsdb.pixels.sink.deserializer; +package io.pixelsdb.pixels.sink.event.deserializer; import io.pixelsdb.pixels.sink.event.RowChangeEvent; import org.apache.kafka.common.serialization.Deserializer; @@ -30,11 +30,13 @@ import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertTrue; -class RowChangeEventDeserializerTest { +class RowChangeEventDeserializerTest +{ private final Deserializer deserializer = new RowChangeEventJsonDeserializer(); - private String loadSchemaFromFile(String filename) throws IOException, URISyntaxException { + private String loadSchemaFromFile(String filename) throws IOException, URISyntaxException + { ClassLoader classLoader = getClass().getClassLoader(); return new String(Files.readAllBytes(Paths.get( Objects.requireNonNull(classLoader.getResource(filename)).toURI() @@ -58,7 +60,8 @@ private String loadSchemaFromFile(String filename) throws IOException, URISyntax // } @Test - void shouldHandleDeleteOperation() throws Exception { + void shouldHandleDeleteOperation() throws Exception + { String jsonData = loadSchemaFromFile("records/delete.json"); RowChangeEvent event = deserializer.deserialize("test_topic", jsonData.getBytes()); @@ -69,7 +72,8 @@ void shouldHandleDeleteOperation() throws Exception { @Test - void shouldHandleEmptyData() { + void shouldHandleEmptyData() + { RowChangeEvent event = deserializer.deserialize("empty_topic", new byte[0]); assertNull(event); } diff --git a/src/test/java/io/pixelsdb/pixels/sink/event/deserializer/RowDataParserTest.java b/src/test/java/io/pixelsdb/pixels/sink/event/deserializer/RowDataParserTest.java new file mode 100644 index 0000000..0eee54b --- /dev/null +++ b/src/test/java/io/pixelsdb/pixels/sink/event/deserializer/RowDataParserTest.java @@ -0,0 +1,58 @@ +/* + * 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.event.deserializer; + +import io.pixelsdb.pixels.sink.util.DateUtil; +import org.junit.jupiter.api.Test; + +import java.time.Instant; +import java.time.LocalDateTime; +import java.time.ZoneOffset; +import java.time.ZonedDateTime; +import java.util.Date; + +public class RowDataParserTest +{ + // BqQ= 17 + // JbR7 24710.35 +// @ParameterizedTest +// @CsvSource({ +// // encodedValue, expectedValue, precision, scale +// "BqQ=, 17.00, 15, 2", +// "JbR7, 24710.35, 15, 2", +// }) +// void testParseDecimalValid(String encodedValue, String expectedValue, int precision, int scale) { +// JsonNode node = new TextNode(encodedValue); +// TypeDescription type = TypeDescription.createDecimal(precision, scale); +// RowDataParser rowDataParser = new RowDataParser(type); +// BigDecimal result = rowDataParser.parseDecimal(node, type); +// assertEquals(new BigDecimal(expectedValue), result); +// } + + @Test + void testParseDate() + { + int day = 17059; + Date debeziumDate = DateUtil.fromDebeziumDate(day); + String dayString = DateUtil.convertDateToDayString(debeziumDate); + long ts = 1473927308302000L; + LocalDateTime localDateTime = LocalDateTime.ofInstant(Instant.ofEpochMilli(ts / 1000), ZoneOffset.UTC); + ZonedDateTime zonedDateTime = Instant.ofEpochMilli(ts).atZone(ZoneOffset.UTC); + boolean pause = true; + } +} diff --git a/src/test/java/io/pixelsdb/pixels/sink/deserializer/SchemaDeserializerTest.java b/src/test/java/io/pixelsdb/pixels/sink/event/deserializer/SchemaDeserializerTest.java similarity index 88% rename from src/test/java/io/pixelsdb/pixels/sink/deserializer/SchemaDeserializerTest.java rename to src/test/java/io/pixelsdb/pixels/sink/event/deserializer/SchemaDeserializerTest.java index f95b9c1..bdd14e4 100644 --- a/src/test/java/io/pixelsdb/pixels/sink/deserializer/SchemaDeserializerTest.java +++ b/src/test/java/io/pixelsdb/pixels/sink/event/deserializer/SchemaDeserializerTest.java @@ -15,7 +15,7 @@ * */ -package io.pixelsdb.pixels.sink.deserializer; +package io.pixelsdb.pixels.sink.event.deserializer; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; @@ -30,22 +30,26 @@ import java.nio.file.Paths; import java.util.Objects; -public class SchemaDeserializerTest { +public class SchemaDeserializerTest +{ private static final ObjectMapper objectMapper = new ObjectMapper(); @BeforeEach - public void setUp() throws IOException, URISyntaxException { + public void setUp() throws IOException, URISyntaxException + { // String schemaStr = schemaNode.toString(); } - private String loadSchemaFromFile(String filename) throws IOException, URISyntaxException { + private String loadSchemaFromFile(String filename) throws IOException, URISyntaxException + { ClassLoader classLoader = getClass().getClassLoader(); return new String(Files.readAllBytes(Paths.get(Objects.requireNonNull(classLoader.getResource(filename)).toURI()))); } @Test - public void testParseNationStruct() throws IOException, URISyntaxException { + public void testParseNationStruct() throws IOException, URISyntaxException + { String jsonSchema = loadSchemaFromFile("records/nation.json"); JsonNode rootNode = objectMapper.readTree(jsonSchema); JsonNode schemaNode = rootNode.get("schema"); @@ -65,7 +69,8 @@ public void testParseNationStruct() throws IOException, URISyntaxException { } @Test - public void testParseDecimalType() throws IOException { + public void testParseDecimalType() throws IOException + { String jsonSchema = "[{" + "\"type\": \"bytes\"," + "\"name\": \"org.apache.kafka.connect.data.Decimal\"," @@ -86,7 +91,8 @@ public void testParseDecimalType() throws IOException { } @Test - public void testParseDateType() throws IOException { + public void testParseDateType() throws IOException + { String jsonSchema = "[{" + "\"type\": \"int32\"," @@ -101,21 +107,24 @@ public void testParseDateType() throws IOException { // 测试未知类型异常 @Test - public void testParseInvalidType() throws IOException { + public void testParseInvalidType() throws IOException + { String jsonSchema = "[{" + "\"type\": \"unknown_type\"," + "\"field\": \"invalid_field\"" + "}]"; JsonNode rootNode = objectMapper.readTree(jsonSchema); Assertions.assertThrows( - IllegalArgumentException.class, () -> { + IllegalArgumentException.class, () -> + { SchemaDeserializer.parseStruct(rootNode); } ); } @Test - public void testMissingRequiredField() throws IOException { + public void testMissingRequiredField() throws IOException + { String jsonSchema = "{" + "\"type\": \"struct\"," + "\"fields\": [" @@ -124,7 +133,8 @@ public void testMissingRequiredField() throws IOException { + "}"; JsonNode rootNode = objectMapper.readTree(jsonSchema); Assertions.assertThrows( - IllegalArgumentException.class, () -> { + IllegalArgumentException.class, () -> + { SchemaDeserializer.parseFieldType(rootNode); } ); diff --git a/src/test/java/io/pixelsdb/pixels/sink/freshness/TestFreshnessClient.java b/src/test/java/io/pixelsdb/pixels/sink/freshness/TestFreshnessClient.java new file mode 100644 index 0000000..da55de3 --- /dev/null +++ b/src/test/java/io/pixelsdb/pixels/sink/freshness/TestFreshnessClient.java @@ -0,0 +1,83 @@ +/* + * 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.freshness; + +import io.pixelsdb.pixels.sink.config.PixelsSinkConfig; +import io.pixelsdb.pixels.sink.config.factory.PixelsSinkConfigFactory; +import io.pixelsdb.pixels.sink.util.MetricsFacade; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.MockedStatic; +import org.mockito.Mockito; + +import java.io.IOException; +import java.lang.reflect.Field; +import java.sql.Connection; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Statement; +import java.util.concurrent.ScheduledExecutorService; + +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.*; + +// We extend FreshnessClient to access the protected queryAndCalculateFreshness method +public class TestFreshnessClient +{ + + // Mocks for JDBC dependencies + private Connection mockConnection; + private Statement mockStatement; + private ResultSet mockResultSet; + + // Mocks for utility/config dependencies + private PixelsSinkConfig mockConfig; + private MetricsFacade mockMetricsFacade; + private FreshnessClient client; // The instance of the client to test + + @BeforeAll + public static void setUp() throws IOException + { + // Initialization as per the user's template + PixelsSinkConfigFactory.initialize("/home/ubuntu/pixels-sink/conf/pixels-sink.aws.properties"); + } + @Test + public void testFreshnessCalculationSuccess() throws Exception { + + FreshnessClient freshnessClient = FreshnessClient.getInstance(); + freshnessClient.addMonitoredTable("customer"); + freshnessClient.start(); + while(true){} + } + + @Test + public void testSnapshotTs() throws SQLException + { + FreshnessClient freshnessClient = FreshnessClient.getInstance(); + Connection connection = freshnessClient.createNewConnection(123456L); + String query = String.format("SELECT max(freshness_ts) FROM customer"); + Statement statement = connection.createStatement(); + ResultSet resultSet = statement.executeQuery(query); + resultSet.next(); + } +} \ No newline at end of file diff --git a/src/test/java/io/pixelsdb/pixels/sink/metadata/TestIndexService.java b/src/test/java/io/pixelsdb/pixels/sink/metadata/TestIndexService.java new file mode 100644 index 0000000..a69a933 --- /dev/null +++ b/src/test/java/io/pixelsdb/pixels/sink/metadata/TestIndexService.java @@ -0,0 +1,168 @@ +/* + * 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.metadata; + +import com.google.protobuf.ByteString; +import io.pixelsdb.pixels.common.exception.IndexException; +import io.pixelsdb.pixels.common.exception.MetadataException; +import io.pixelsdb.pixels.common.index.IndexService; +import io.pixelsdb.pixels.common.metadata.MetadataService; +import io.pixelsdb.pixels.common.metadata.domain.Layout; +import io.pixelsdb.pixels.common.metadata.domain.SinglePointIndex; +import io.pixelsdb.pixels.common.metadata.domain.Table; +import io.pixelsdb.pixels.daemon.MetadataProto; +import io.pixelsdb.pixels.index.IndexProto; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +import java.nio.ByteBuffer; + +/** + * @package: io.pixelsdb.pixels.sink.metadata + * @className: TestIndexService + * @author: AntiO2 + * @date: 2025/8/5 04:34 + */ +public class TestIndexService +{ + + private final MetadataService metadataService = MetadataService.Instance(); + private final IndexService indexService = null; // TODO + + @Test + public void testCreateFreshnessIndex() throws MetadataException + { + String testSchemaName = "pixels_bench_sf1x"; + String testTblName = "freshness"; + String keyColumn = "{\"keyColumnIds\":[15]}"; + Table table = metadataService.getTable(testSchemaName, testTblName); + Layout layout = metadataService.getLatestLayout(testSchemaName, testTblName); + + MetadataProto.SinglePointIndex.Builder singlePointIndexbuilder = MetadataProto.SinglePointIndex.newBuilder(); + singlePointIndexbuilder.setId(0L) + .setKeyColumns(keyColumn) + .setPrimary(true) + .setUnique(true) + .setIndexScheme("rocksdb") + .setTableId(table.getId()) + .setSchemaVersionId(layout.getSchemaVersionId()); + + SinglePointIndex index = new SinglePointIndex(singlePointIndexbuilder.build()); + boolean result = metadataService.createSinglePointIndex(index); + Assertions.assertTrue(result); + boolean pause = true; + } + + @Test + public void testCreateIndex() throws MetadataException + { + String testSchemaName = "pixels_index"; + String testTblName = "ray_index"; + String keyColumn = "{\"keyColumnIds\":[11]}"; + Table table = metadataService.getTable(testSchemaName, testTblName); + long id = table.getId(); + long schemaId = table.getSchemaId(); + Layout layout = metadataService.getLatestLayout(testSchemaName, testTblName); + + MetadataProto.SinglePointIndex.Builder singlePointIndexbuilder = MetadataProto.SinglePointIndex.newBuilder(); + singlePointIndexbuilder.setId(0L) + .setKeyColumns(keyColumn) + .setPrimary(true) + .setUnique(true) + .setIndexScheme("rocksdb") + .setTableId(table.getId()) + .setSchemaVersionId(layout.getSchemaVersionId()); + + SinglePointIndex index = new SinglePointIndex(singlePointIndexbuilder.build()); + boolean result = metadataService.createSinglePointIndex(index); + Assertions.assertTrue(result); + boolean pause = true; + } + + @Test + public void testGetIndex() throws MetadataException + { + String testSchemaName = "pixels_index"; + String testTblName = "ray_index"; + Table table = metadataService.getTable(testSchemaName, testTblName); + long id = table.getId(); + SinglePointIndex index = metadataService.getPrimaryIndex(id); + + Assertions.assertNotNull(index); + boolean pause = true; + } + + @Test + public void testGetRowID() throws MetadataException, IndexException + { + int numRowIds = 10000; + IndexProto.RowIdBatch rowIdBatch = indexService.allocateRowIdBatch(4, numRowIds); + Assertions.assertEquals(rowIdBatch.getLength(), numRowIds); + boolean pause = true; + } + + @Test + public void testPutAndDelete() throws MetadataException, IndexException + { + String table = "customer"; + String db = "pixels_bench_sf1x"; + Table table1 = metadataService.getTable(db, table); + long tableId = table1.getId(); + SinglePointIndex index = metadataService.getPrimaryIndex(tableId); + + String id = "2294222"; + + int len = 1; + int keySize = 0; + keySize += id.length(); + keySize += Long.BYTES + (len + 1) * 2; // table id + index key + + ByteBuffer byteBuffer = ByteBuffer.allocate(keySize); + + byteBuffer.putLong(index.getTableId()).putChar(':'); + byteBuffer.put(id.getBytes()); + byteBuffer.putChar(':'); + + + IndexProto.PrimaryIndexEntry.Builder builder = IndexProto.PrimaryIndexEntry.newBuilder(); + long ts1 = 200000; + long ts2 = 100000; + int rgId = 100; + int rgoffset = 10; + + builder.getIndexKeyBuilder() + .setTimestamp(ts1) + .setKey(ByteString.copyFrom(byteBuffer.rewind())) + .setIndexId(index.getId()) + .setTableId(index.getTableId()); + builder.setRowId(100); + builder.getRowLocationBuilder() + .setRgId(rgId) + .setFileId(0) + .setRgRowOffset(rgoffset); + + boolean b = indexService.putPrimaryIndexEntry(builder.build()); + + builder.getIndexKeyBuilder().setTimestamp(ts2); + + IndexProto.RowLocation rowLocation = indexService.deletePrimaryIndexEntry(builder.getIndexKey()); + + boolean pause = false; + } +} diff --git a/src/test/java/io/pixelsdb/pixels/sink/util/EtcdFileRegistryTest.java b/src/test/java/io/pixelsdb/pixels/sink/util/EtcdFileRegistryTest.java new file mode 100644 index 0000000..aee4a0e --- /dev/null +++ b/src/test/java/io/pixelsdb/pixels/sink/util/EtcdFileRegistryTest.java @@ -0,0 +1,54 @@ +/* + * 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.util; + + +import org.junit.jupiter.api.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.List; + +/** + * @package: io.pixelsdb.pixels.sink.util + * @className: EtcdFileRegistryTest + * @author: AntiO2 + * @date: 2025/10/5 08:54 + */ +public class EtcdFileRegistryTest +{ + private static final Logger LOGGER = LoggerFactory.getLogger(EtcdFileRegistryTest.class); + + @Test + public void testCreateFile() + { + EtcdFileRegistry etcdFileRegistry = new EtcdFileRegistry("test", "file:///tmp/test/ray"); + for (int i = 0; i < 10; i++) + { + String newFile = etcdFileRegistry.createNewFile(); + etcdFileRegistry.markFileCompleted(newFile); + } + List files = etcdFileRegistry.listAllFiles(); + for (String file : files) + { + LOGGER.info(file); + } + etcdFileRegistry.cleanData(); + } +} diff --git a/src/test/java/io/pixelsdb/pixels/sink/writer/RpcEndToEndTest.java b/src/test/java/io/pixelsdb/pixels/sink/writer/RpcEndToEndTest.java new file mode 100644 index 0000000..0a86e12 --- /dev/null +++ b/src/test/java/io/pixelsdb/pixels/sink/writer/RpcEndToEndTest.java @@ -0,0 +1,224 @@ +/* + * 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.writer; + +import com.google.protobuf.ByteString; +import io.grpc.ManagedChannel; +import io.grpc.ManagedChannelBuilder; +import io.pixelsdb.pixels.sink.event.RowChangeEvent; +import io.pixelsdb.pixels.sink.writer.flink.FlinkPollingWriter; +import io.pixelsdb.pixels.sink.PixelsPollingServiceGrpc; +import io.pixelsdb.pixels.sink.SinkProto; +import io.pixelsdb.pixels.sink.config.PixelsSinkConfig; +import io.pixelsdb.pixels.sink.config.CommandLineConfig; +import io.pixelsdb.pixels.sink.config.factory.PixelsSinkConfigFactory; +import io.pixelsdb.pixels.sink.source.SinkSource; +import io.pixelsdb.pixels.sink.source.SinkSourceFactory; +import io.pixelsdb.pixels.sink.exception.SinkException; +import io.pixelsdb.pixels.sink.writer.retina.SinkContextManager; +import io.pixelsdb.pixels.sink.writer.retina.TransactionProxy; +import io.pixelsdb.pixels.sink.writer.PixelsSinkWriterFactory; +import io.pixelsdb.pixels.sink.util.MetricsFacade; +import io.prometheus.client.exporter.HTTPServer; +import io.prometheus.client.hotspot.DefaultExports; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; + +public class RpcEndToEndTest +{ + private static final int TEST_PORT = 9091; + private static final int RECORD_COUNT = 5; + private static SinkSource sinkSource; // Keep a reference to stop it later + private static final String TEST_SCHEMA = "pixels_bench_sf10x"; + private static final String TEST_TABLE = "savingaccount"; + private static final String FULL_TABLE_NAME = TEST_SCHEMA + "." + TEST_TABLE; + private static final String CONFIG_FILE_PATH = "conf/pixels-sink.aws.properties"; + private static HTTPServer prometheusHttpServer; + // This method contains the setup logic from PixelsSinkApp + private static void startServer() + { + System.out.println("[SETUP] Starting server with full initialization sequence..."); + try { + // === 1. Mimic init() method from PixelsSinkApp === + System.out.println("[SETUP] Initializing configuration from " + CONFIG_FILE_PATH + "..."); + PixelsSinkConfigFactory.initialize(CONFIG_FILE_PATH); + + System.out.println("[SETUP] Initializing MetricsFacade..."); + MetricsFacade.getInstance().setSinkContextManager(SinkContextManager.getInstance()); + + // For determinism in testing, override the port from the config file + System.setProperty("sink.flink.server.port", String.valueOf(TEST_PORT)); + // === 2. Mimic main() method from PixelsSinkApp === + PixelsSinkConfig config = PixelsSinkConfigFactory.getInstance(); + System.out.println("[SETUP] Creating SinkSource application engine..."); + sinkSource = SinkSourceFactory.createSinkSource(); + System.out.println("[SETUP] Setting up Prometheus monitoring server..."); + if (config.isMonitorEnabled()) { + DefaultExports.initialize(); + // To avoid port conflicts during tests, you could use port 0 for a random port + // For this example, we'll assume the config port is fine. + prometheusHttpServer = new HTTPServer(config.getMonitorPort()); + System.out.println("[SETUP] Prometheus server started on port: " + config.getMonitorPort()); + } + // === 3. THE MOST CRITICAL STEP: Start the processor === + System.out.println("[SETUP] Starting SinkSource processor threads..."); + sinkSource.start(); + System.out.println("[SETUP] Server is fully initialized and running."); + } catch (IOException e) { + // If setup fails, we cannot run the test. + throw new RuntimeException("Failed to start the server for the test", e); + } + } + public static void main(String[] args) throws InterruptedException, IOException + { + // ========== 1. SETUP PHASE ========== + // Start the server components within this same process. + startServer(); + + // Give the server a moment to fully start up and bind to the port. + Thread.sleep(2000); // 2 seconds, adjust if needed + // [REFACTORED] To ensure the FlinkPollingWriter uses our test port, + // we set it as a system property before the writer is created. + // The PixelsSinkConfigFactory should be configured to read this property. + System.setProperty("sink.flink.server.port", String.valueOf(TEST_PORT)); + + // [REFACTORED] The setup is now dramatically simpler. + // Instantiating FlinkPollingWriter is the ONLY step needed. + // Its constructor automatically creates the service and starts the gRPC server. + System.out.println("[SETUP] Initializing FlinkPollingWriter, which will start the gRPC server..."); + PixelsSinkWriter writer = PixelsSinkWriterFactory.getWriter(); + + // [REFACTORED] The following lines are no longer needed and have been removed: + // PixelsPollingServiceImpl serviceImpl = new PixelsPollingServiceImpl(writer); + // PollingRpcServer server = new PollingRpcServer(serviceImpl, TEST_PORT); + // server.start(); + + ExecutorService executor = Executors.newFixedThreadPool(2); + + try { + // 3. Start the mock data producer (in a separate thread) + executor.submit(() -> { + try { + System.out.println("[PRODUCER] Starting mock data producer..."); + // Simulate producing 5 INSERT records + for (int i = 1; i <= 5; i++) { + Thread.sleep(1000); // Simulate data production interval + + SinkProto.SourceInfo sourceInfo = SinkProto.SourceInfo.newBuilder() + .setDb(TEST_SCHEMA) + .setTable(TEST_TABLE) + .build(); + + byte[] idBytes = String.valueOf(i).getBytes(StandardCharsets.UTF_8); + SinkProto.ColumnValue idColumnValue = SinkProto.ColumnValue.newBuilder() + .setValue(ByteString.copyFrom(idBytes)) + .build(); + + SinkProto.RowValue afterImage = SinkProto.RowValue.newBuilder() + .addValues(idColumnValue) + .build(); + + SinkProto.RowRecord record = SinkProto.RowRecord.newBuilder() + .setSource(sourceInfo) + .setOp(SinkProto.OperationType.INSERT) + .setAfter(afterImage) + .build(); + + RowChangeEvent event = new RowChangeEvent(record); + System.out.printf("[PRODUCER] Writing INSERT record %d for table '%s.%s'%n", i, TEST_SCHEMA, TEST_TABLE); + writer.writeRow(event); + } + System.out.println("[PRODUCER] Finished writing data."); + } catch (InterruptedException e) { + System.err.println("[PRODUCER] Producer thread was interrupted."); + Thread.currentThread().interrupt(); + } catch (SinkException e) { + System.err.println("[PRODUCER] SinkException occurred: %s" + e.getMessage()); + } + }); + + // 4. Start the gRPC client (in another thread) + executor.submit(() -> { + System.out.println("[CLIENT] Starting gRPC client..."); + ManagedChannel channel = ManagedChannelBuilder.forAddress("localhost", TEST_PORT) + .usePlaintext() + .build(); + + try { + PixelsPollingServiceGrpc.PixelsPollingServiceBlockingStub client = PixelsPollingServiceGrpc.newBlockingStub(channel); + int recordsPolled = 0; + int maxRetries = 10; + int retryCount = 0; + while (recordsPolled < 5 && retryCount < maxRetries) { + System.out.println("[CLIENT] Sending poll request for table '" + FULL_TABLE_NAME + "'..."); + SinkProto.PollRequest request = SinkProto.PollRequest.newBuilder() + .setSchemaName(TEST_SCHEMA) + .setTableName(TEST_TABLE) + .build(); + SinkProto.PollResponse response = client.pollEvents(request); + + if (response.getRecordsCount() > 0) { + System.out.printf("[CLIENT] SUCCESS: Polled %d record(s).%n", response.getRecordsCount()); + response.getRecordsList().forEach(record -> System.out.println(" -> " + record.toString().trim())); + recordsPolled += response.getRecordsCount(); + } else { + System.out.println("[CLIENT] Polled 0 records, will retry..."); + retryCount++; + } + } + if (recordsPolled == 5) { + System.out.println("[CLIENT] Test finished successfully. Polled all 5 records."); + } else { + System.err.println("[CLIENT] Test FAILED. Did not poll all records in time."); + } + } finally { + channel.shutdownNow(); + } + }); + } finally { + // 5. Wait for the test to complete and clean up resources + executor.shutdown(); + if (!executor.awaitTermination(1, TimeUnit.MINUTES)) { + System.err.println("[CLEANUP] Test timed out."); + executor.shutdownNow(); + } else { + System.out.println("[CLEANUP] Test completed."); + } + + // [REFACTORED] The correct way to stop the server is now by closing the writer. + writer.close(); + System.out.println("[CLEANUP] FlinkPollingWriter closed and server stopped."); + } + // ========== 3. CLEANUP PHASE ========== + System.out.println("[CLEANUP] Tearing down server components..."); + if (sinkSource != null) { + sinkSource.stopProcessor(); + System.out.println("[CLEANUP] SinkSource processor stopped."); + // Note: The writer is part of sinkSource, and its resources + // should be cleaned up by stopProcessor or an equivalent close method. + } + if (prometheusHttpServer != null) { + prometheusHttpServer.close(); + System.out.println("[CLEANUP] Prometheus server stopped."); + } + System.out.println("[CLEANUP] Test finished."); + } +} diff --git a/src/test/java/io/pixelsdb/pixels/sink/writer/TestProtoWriter.java b/src/test/java/io/pixelsdb/pixels/sink/writer/TestProtoWriter.java new file mode 100644 index 0000000..0c932a3 --- /dev/null +++ b/src/test/java/io/pixelsdb/pixels/sink/writer/TestProtoWriter.java @@ -0,0 +1,166 @@ +/* + * 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.writer; + + +import com.google.protobuf.ByteString; +import io.pixelsdb.pixels.common.physical.*; +import io.pixelsdb.pixels.sink.SinkProto; +import io.pixelsdb.pixels.sink.config.factory.PixelsSinkConfigFactory; +import io.pixelsdb.pixels.sink.writer.proto.ProtoWriter; +import io.pixelsdb.pixels.storage.localfs.PhysicalLocalReader; +import lombok.SneakyThrows; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.nio.charset.StandardCharsets; + +/** + * @package: io.pixelsdb.pixels.sink.writer + * @className: TestProtoWriter + * @author: AntiO2 + * @date: 2025/10/5 09:24 + */ +public class TestProtoWriter +{ + public static String schemaName = "test"; + public static String tableName = "ray"; + + @BeforeAll + public static void setUp() throws IOException + { + PixelsSinkConfigFactory.initialize("/home/pixels/projects/pixels-writer/src/main/resources/pixels-writer.local.properties"); +// PixelsSinkConfigFactory.initialize("/home/ubuntu/pixels-writer/src/main/resources/pixels-writer.aws.properties"); + } + + private static SinkProto.RowRecord getRowRecord(int i) + { + byte[][] cols = new byte[3][]; + + cols[0] = Integer.toString(i).getBytes(StandardCharsets.UTF_8); + cols[1] = Long.toString(i * 1000L).getBytes(StandardCharsets.UTF_8); + cols[2] = ("row_" + i).getBytes(StandardCharsets.UTF_8); + SinkProto.RowValue.Builder afterValueBuilder = SinkProto.RowValue.newBuilder(); + afterValueBuilder + .addValues( + SinkProto.ColumnValue.newBuilder().setValue(ByteString.copyFrom((cols[0]))).build()) + .addValues( + SinkProto.ColumnValue.newBuilder().setValue(ByteString.copyFrom((cols[1]))).build()) + .addValues( + SinkProto.ColumnValue.newBuilder().setValue(ByteString.copyFrom((cols[2]))).build()); + + + SinkProto.RowRecord.Builder builder = SinkProto.RowRecord.newBuilder(); + builder.setOp(SinkProto.OperationType.INSERT) + .setAfter(afterValueBuilder) + .setSource( + SinkProto.SourceInfo.newBuilder() + .setDb(schemaName) + .setTable(tableName) + .build() + ); + return builder.build(); + } + + private static SinkProto.TransactionMetadata getTrans(int i, SinkProto.TransactionStatus status) + { + SinkProto.TransactionMetadata.Builder builder = SinkProto.TransactionMetadata.newBuilder(); + builder.setId(Integer.toString(i)); + builder.setStatus(status); + builder.setTimestamp(System.currentTimeMillis()); + return builder.build(); + } + + @SneakyThrows + @Test + public void testWriteTransInfo() + { + ProtoWriter transWriter = new ProtoWriter(); + int maxTx = 1000; + + for (int i = 0; i < maxTx; i++) + { + transWriter.writeTrans(getTrans(i, SinkProto.TransactionStatus.BEGIN)); + transWriter.writeTrans(getTrans(i, SinkProto.TransactionStatus.END)); + } + transWriter.close(); + } + + @Test + public void testWriteFile() throws IOException + { + String path = "/home/pixels/projects/pixels-writer/tmp/write.dat"; + PhysicalWriter writer = PhysicalWriterUtil.newPhysicalWriter(Storage.Scheme.file, path); + + int writeNum = 3; + + ByteBuffer buf = ByteBuffer.allocate(writeNum * Integer.BYTES); + for (int i = 0; i < 3; i++) + { + buf.putInt(i); + } + writer.append(buf); + writer.close(); + } + + @Test + public void testReadFile() throws IOException + { + String path = "/home/pixels/projects/pixels-writer/tmp/write.dat"; + PhysicalLocalReader reader = (PhysicalLocalReader) PhysicalReaderUtil.newPhysicalReader(Storage.Scheme.file, path); + + int writeNum = 12; + for (int i = 0; i < writeNum; i++) + { + reader.readLong(ByteOrder.BIG_ENDIAN); + } + } + + @Test + public void testReadEmptyFile() throws IOException + { + String path = "/home/pixels/projects/pixels-writer/tmp/empty.dat"; + PhysicalReader reader = PhysicalReaderUtil.newPhysicalReader(Storage.Scheme.file, path); + + int v = reader.readInt(ByteOrder.BIG_ENDIAN); + + return; + } + + @Test + public void testWriteRowInfo() throws IOException + { + ProtoWriter transWriter = new ProtoWriter(); + int maxTx = 10000000; + int rowCnt = 0; + for (int i = 0; i < maxTx; i++) + { + transWriter.writeTrans(getTrans(i, SinkProto.TransactionStatus.BEGIN)); + for (int j = i; j < 3; j++) + { + transWriter.write(getRowRecord(rowCnt++)); + } + transWriter.writeTrans(getTrans(i, SinkProto.TransactionStatus.END)); + } + transWriter.close(); + } +} diff --git a/src/test/java/io/pixelsdb/pixels/sink/writer/TestRetinaWriter.java b/src/test/java/io/pixelsdb/pixels/sink/writer/TestRetinaWriter.java new file mode 100644 index 0000000..15e43ea --- /dev/null +++ b/src/test/java/io/pixelsdb/pixels/sink/writer/TestRetinaWriter.java @@ -0,0 +1,602 @@ +/* + * 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.writer; + +import com.google.protobuf.ByteString; +import io.pixelsdb.pixels.common.exception.RetinaException; +import io.pixelsdb.pixels.common.exception.TransException; +import io.pixelsdb.pixels.common.retina.RetinaService; +import io.pixelsdb.pixels.common.transaction.TransContext; +import io.pixelsdb.pixels.common.transaction.TransService; +import io.pixelsdb.pixels.index.IndexProto; +import io.pixelsdb.pixels.retina.RetinaProto; +import io.pixelsdb.pixels.sink.SinkProto; +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.DateUtil; +import io.pixelsdb.pixels.sink.writer.retina.RetinaServiceProxy; +import io.pixelsdb.pixels.sink.writer.retina.TransactionProxy; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.List; +import java.util.Random; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +public class TestRetinaWriter +{ + + static Logger logger = LoggerFactory.getLogger(TestRetinaWriter.class.getName()); + static RetinaService retinaService; + static TableMetadataRegistry metadataRegistry; + static TransService transService; + static int retinaPerformanceTestRowCount; + static int retinaPerformanceTestMaxId; + private final ExecutorService executor = Executors.newFixedThreadPool(16); + + @BeforeAll + public static void setUp() throws IOException + { + PixelsSinkConfigFactory.initialize("/home/pixels/projects/pixels-sink/src/main/resources/pixels-sink.local.properties"); +// PixelsSinkConfigFactory.initialize("/home/ubuntu/pixels-sink/src/main/resources/pixels-sink.aws.properties"); + retinaService = RetinaService.Instance(); + metadataRegistry = TableMetadataRegistry.Instance(); + transService = TransService.Instance(); + retinaPerformanceTestRowCount = 5_000_000; + retinaPerformanceTestMaxId = 2_000_000; + } + + @Test + public void insertSingleRecord() throws RetinaException, SinkException, TransException + { + + TransContext ctx = transService.beginTrans(false); + long timeStamp = ctx.getTimestamp(); + String schemaName = "pixels_index"; + String tableName = "ray_index"; + List tableUpdateData = new ArrayList<>(); + RetinaProto.TableUpdateData.Builder tableUpdateDataBuilder = RetinaProto.TableUpdateData.newBuilder(); + tableUpdateDataBuilder.setTableName(tableName); + tableUpdateDataBuilder.setPrimaryIndexId(metadataRegistry.getPrimaryIndexKeyId(schemaName, tableName)); + // + for (int i = 0; i < 10; ++i) + { + byte[][] cols = new byte[3][]; + + cols[0] = Integer.toString(i).getBytes(StandardCharsets.UTF_8); + cols[1] = Long.toString(i * 1000L).getBytes(StandardCharsets.UTF_8); + cols[2] = ("row_" + i).getBytes(StandardCharsets.UTF_8); + SinkProto.RowValue.Builder afterValueBuilder = SinkProto.RowValue.newBuilder(); + afterValueBuilder + .addValues( + SinkProto.ColumnValue.newBuilder().setValue(ByteString.copyFrom((cols[0]))).build()) + .addValues( + SinkProto.ColumnValue.newBuilder().setValue(ByteString.copyFrom((cols[1]))).build()) + .addValues( + SinkProto.ColumnValue.newBuilder().setValue(ByteString.copyFrom((cols[2]))).build()); + + + SinkProto.RowRecord.Builder builder = SinkProto.RowRecord.newBuilder(); + builder.setOp(SinkProto.OperationType.INSERT) + .setAfter(afterValueBuilder) + .setSource( + SinkProto.SourceInfo.newBuilder() + .setDb(schemaName) + .setTable(tableName) + .build() + ); + RowChangeEvent rowChangeEvent = new RowChangeEvent(builder.build()); + rowChangeEvent.setTimeStamp(timeStamp); + IndexProto.IndexKey indexKey = rowChangeEvent.getAfterKey(); + RetinaProto.InsertData.Builder insertDataBuilder = RetinaProto.InsertData.newBuilder(); + insertDataBuilder. + addColValues(ByteString.copyFrom((cols[0]))).addColValues(ByteString.copyFrom((cols[1]))) + .addColValues(ByteString.copyFrom((cols[2]))) + .addIndexKeys(indexKey); + tableUpdateDataBuilder.addInsertData(insertDataBuilder.build()); + } + tableUpdateData.add(tableUpdateDataBuilder.build()); + retinaService.updateRecord(schemaName, 0, tableUpdateData); + tableUpdateDataBuilder.setTimestamp(timeStamp); + transService.commitTrans(ctx.getTransId(), false); + } + + @Test + public void updateSingleRecord() throws RetinaException, SinkException, TransException + { + TransContext ctx = transService.beginTrans(false); + long timeStamp = ctx.getTimestamp(); + String schemaName = "pixels_index"; + String tableName = "ray_index"; + + + List tableUpdateData = new ArrayList<>(); + RetinaProto.TableUpdateData.Builder tableUpdateDataBuilder = RetinaProto.TableUpdateData.newBuilder(); + tableUpdateDataBuilder.setTableName(tableName); + tableUpdateDataBuilder.setPrimaryIndexId(metadataRegistry.getPrimaryIndexKeyId(schemaName, tableName)); + // + for (int i = 0; i < 10; ++i) + { + byte[][] cols = new byte[4][]; + cols[0] = Integer.toString(i).getBytes(StandardCharsets.UTF_8); + cols[1] = Long.toString(i * 1000L).getBytes(StandardCharsets.UTF_8); + cols[2] = ("row_" + i).getBytes(StandardCharsets.UTF_8); + cols[3] = ("updated_row_" + i).getBytes(StandardCharsets.UTF_8); + SinkProto.RowValue.Builder beforeValueBuilder = SinkProto.RowValue.newBuilder(); + beforeValueBuilder + .addValues( + SinkProto.ColumnValue.newBuilder().setValue(ByteString.copyFrom((cols[0]))).build()) + .addValues( + SinkProto.ColumnValue.newBuilder().setValue(ByteString.copyFrom((cols[1]))).build()) + .addValues( + SinkProto.ColumnValue.newBuilder().setValue(ByteString.copyFrom((cols[2]))).build()); + + + SinkProto.RowValue.Builder afterValueBuilder = SinkProto.RowValue.newBuilder(); + afterValueBuilder + .addValues( + SinkProto.ColumnValue.newBuilder().setValue(ByteString.copyFrom((cols[0]))).build()) + .addValues( + SinkProto.ColumnValue.newBuilder().setValue(ByteString.copyFrom((cols[1]))).build()) + .addValues( + SinkProto.ColumnValue.newBuilder().setValue(ByteString.copyFrom((cols[3]))).build()); + SinkProto.RowRecord.Builder builder = SinkProto.RowRecord.newBuilder(); + builder.setOp(SinkProto.OperationType.UPDATE) + .setBefore(beforeValueBuilder) + .setAfter(afterValueBuilder) + .setSource( + SinkProto.SourceInfo.newBuilder() + .setDb(schemaName) + .setTable(tableName) + .build() + ); + + RowChangeEvent rowChangeEvent = new RowChangeEvent(builder.build()); + rowChangeEvent.setTimeStamp(timeStamp); + RetinaProto.DeleteData.Builder deleteDataBuilder = RetinaProto.DeleteData.newBuilder(); + deleteDataBuilder + .addIndexKeys(rowChangeEvent.getBeforeKey()); + tableUpdateDataBuilder.addDeleteData(deleteDataBuilder.build()); + + RetinaProto.InsertData.Builder insertDataBuilder = RetinaProto.InsertData.newBuilder(); + insertDataBuilder + .addColValues(ByteString.copyFrom((cols[0]))).addColValues(ByteString.copyFrom((cols[1]))) + .addColValues(ByteString.copyFrom((cols[3]))) + .addIndexKeys(rowChangeEvent.getAfterKey()); + tableUpdateDataBuilder.addInsertData(insertDataBuilder.build()); + } + tableUpdateDataBuilder.setTimestamp(timeStamp); + tableUpdateData.add(tableUpdateDataBuilder.build()); + retinaService.updateRecord(schemaName, 0, tableUpdateData); + transService.commitTrans(ctx.getTransId(), false); + } + + @Test + public void testCheckingAccountInsertPerformance() throws + RetinaException, SinkException, TransException, IOException, ExecutionException, InterruptedException + { + String schemaName = "pixels_bench_sf1x"; + String tableName = "savingaccount"; + + RetinaServiceProxy writer = new RetinaServiceProxy(-1); + + TransactionProxy manager = TransactionProxy.Instance(); + // Step 1: Insert 10,000 records + int totalInserts = retinaPerformanceTestMaxId; + int batchSize = 5; + int batchCount = totalInserts / batchSize; + + int samllBatchCount = 10; + + long start = System.currentTimeMillis(); + + List> futures = new ArrayList<>(); + + for (int b = 0; b < batchCount; ) + { + List tableUpdateData = new ArrayList<>(); + for (int sb = 0; sb < samllBatchCount; sb++) + { + ++b; + TransContext ctx = manager.getNewTransContext(); + long timeStamp = ctx.getTimestamp(); + + RetinaProto.TableUpdateData.Builder tableUpdateDataBuilder = + RetinaProto.TableUpdateData.newBuilder() + .setTableName(tableName) + .setPrimaryIndexId(metadataRegistry.getPrimaryIndexKeyId(schemaName, tableName)); + + for (int i = 0; i < batchSize; i++) + { + int accountID = b * batchSize + i; + int userID = accountID % 1000; + float balance = 1000.0f + accountID; + int isBlocked = 0; + long ts = System.currentTimeMillis(); + + byte[][] cols = new byte[5][]; + cols[0] = ByteBuffer.allocate(Integer.BYTES).putInt(accountID).array(); + cols[1] = ByteBuffer.allocate(Integer.BYTES).putInt(userID).array(); + int intBits = Float.floatToIntBits(balance); + cols[2] = ByteBuffer.allocate(4).putInt(intBits).array(); + cols[3] = ByteBuffer.allocate(Integer.BYTES).putInt(isBlocked).array(); + cols[4] = ByteBuffer.allocate(Long.BYTES).putLong(ts).array(); + // cols[4] = Long.toString(ts).getBytes(StandardCharsets.UTF_8); + // after row + SinkProto.RowValue.Builder afterValueBuilder = SinkProto.RowValue.newBuilder() + .addValues(SinkProto.ColumnValue.newBuilder().setValue(ByteString.copyFrom(cols[0])).build()) + .addValues(SinkProto.ColumnValue.newBuilder().setValue(ByteString.copyFrom(cols[1])).build()) + .addValues(SinkProto.ColumnValue.newBuilder().setValue(ByteString.copyFrom(cols[2])).build()) + .addValues(SinkProto.ColumnValue.newBuilder().setValue(ByteString.copyFrom(cols[3])).build()) + .addValues(SinkProto.ColumnValue.newBuilder().setValue(ByteString.copyFrom(cols[4])).build()); + + // RowRecord + SinkProto.RowRecord.Builder rowBuilder = SinkProto.RowRecord.newBuilder() + .setOp(SinkProto.OperationType.INSERT) + .setAfter(afterValueBuilder) + .setSource( + SinkProto.SourceInfo.newBuilder() + .setDb(schemaName) + .setTable(tableName) + .build() + ); + + RowChangeEvent rowChangeEvent = new RowChangeEvent(rowBuilder.build()); + rowChangeEvent.setTimeStamp(timeStamp); + + // InsertData + RetinaProto.InsertData.Builder insertDataBuilder = RetinaProto.InsertData.newBuilder() + .addColValues(ByteString.copyFrom(cols[0])) + .addColValues(ByteString.copyFrom(cols[1])) + .addColValues(ByteString.copyFrom(cols[2])) + .addColValues(ByteString.copyFrom(cols[3])) + .addColValues(ByteString.copyFrom(cols[4])) + .addIndexKeys(rowChangeEvent.getAfterKey()); + + tableUpdateDataBuilder.addInsertData(insertDataBuilder.build()); + } + + tableUpdateData.add(tableUpdateDataBuilder.build()); + + CompletableFuture future = CompletableFuture.runAsync(() -> + { + try + { + transService.commitTrans(ctx.getTransId(), false); + } catch (TransException e) + { + e.printStackTrace(); + throw new RuntimeException(e); + } + + }, executor); + futures.add(future); + } + Assertions.assertNotNull(writer); + if (!writer.writeTrans(schemaName, tableUpdateData)) + { + logger.error("Error Write Trans"); + System.exit(-1); + } + + } + + for (CompletableFuture future : futures) + { + future.get(); + } + + long end = System.currentTimeMillis(); + double seconds = (end - start) / 1000.0; + double insertsPerSec = totalInserts / seconds; + double transPerSec = batchCount / seconds; + logger.info("Inserted " + totalInserts + " rows in " + seconds + "s, rate=" + insertsPerSec + " inserts/s," + transPerSec + "trans/s"); + writer.close(); + } + + + @Test + public void testCheckingAccountUpdatePerformance() throws + RetinaException, SinkException, TransException, IOException, ExecutionException, InterruptedException + { + String schemaName = "pixels_bench_sf1x"; + String tableName = "checkingaccount"; + + int totalUpdates = retinaPerformanceTestRowCount; // 总更新条数 + int batchSize = 5; // 每个事务包含多少条 update + int batchCount = totalUpdates / batchSize; + + int clientCount = 2; // 可自定义客户端数量 + ExecutorService clientExecutor = Executors.newFixedThreadPool(clientCount); + + List writers = new ArrayList<>(); + for (int c = 0; c < clientCount; c++) + { + writers.add(new RetinaServiceProxy(-1)); + } + + Random random = new Random(); + TransactionProxy manager = TransactionProxy.Instance(); + + long start = System.currentTimeMillis(); + List> futures = new ArrayList<>(); + + for (int b = 0; b < batchCount; b++) + { + final int batchIndex = b; + CompletableFuture batchFuture = CompletableFuture.runAsync(() -> + { + try + { + // 轮询选择客户端 + RetinaServiceProxy writer = writers.get(batchIndex % clientCount); + + TransContext ctx = manager.getNewTransContext(); + long timeStamp = ctx.getTimestamp(); + + List tableUpdateData = new ArrayList<>(); + RetinaProto.TableUpdateData.Builder tableUpdateDataBuilder = + RetinaProto.TableUpdateData.newBuilder() + .setTableName(tableName) + .setPrimaryIndexId(metadataRegistry.getPrimaryIndexKeyId(schemaName, tableName)); + + for (int i = 0; i < batchSize; i++) + { + int accountID = random.nextInt(retinaPerformanceTestMaxId); + int userID = accountID % 1000; + float oldBalance = 1000.0f + accountID; + float newBalance = oldBalance + random.nextInt(1000); + int isBlocked = 0; + long oldTs = System.currentTimeMillis() - 1000; + long newTs = System.currentTimeMillis(); + + // 构建 before/after row + SinkProto.RowValue.Builder beforeValueBuilder = SinkProto.RowValue.newBuilder() + .addValues(SinkProto.ColumnValue.newBuilder().setValue(ByteString.copyFromUtf8(Integer.toString(accountID))).build()) + .addValues(SinkProto.ColumnValue.newBuilder().setValue(ByteString.copyFromUtf8(Integer.toString(userID))).build()) + .addValues(SinkProto.ColumnValue.newBuilder().setValue(ByteString.copyFromUtf8(Float.toString(oldBalance))).build()) + .addValues(SinkProto.ColumnValue.newBuilder().setValue(ByteString.copyFromUtf8(Integer.toString(isBlocked))).build()) + .addValues(SinkProto.ColumnValue.newBuilder().setValue(ByteString.copyFromUtf8(DateUtil.convertDebeziumTimestampToString(oldTs))).build()); + + SinkProto.RowValue.Builder afterValueBuilder = SinkProto.RowValue.newBuilder() + .addValues(SinkProto.ColumnValue.newBuilder().setValue(ByteString.copyFromUtf8(Integer.toString(accountID))).build()) + .addValues(SinkProto.ColumnValue.newBuilder().setValue(ByteString.copyFromUtf8(Integer.toString(userID))).build()) + .addValues(SinkProto.ColumnValue.newBuilder().setValue(ByteString.copyFromUtf8(Float.toString(newBalance))).build()) + .addValues(SinkProto.ColumnValue.newBuilder().setValue(ByteString.copyFromUtf8(Integer.toString(isBlocked))).build()) + .addValues(SinkProto.ColumnValue.newBuilder().setValue(ByteString.copyFromUtf8(DateUtil.convertDebeziumTimestampToString(newTs))).build()); + + SinkProto.RowRecord.Builder rowBuilder = SinkProto.RowRecord.newBuilder() + .setOp(SinkProto.OperationType.UPDATE) + .setBefore(beforeValueBuilder) + .setAfter(afterValueBuilder) + .setSource(SinkProto.SourceInfo.newBuilder().setDb(schemaName).setTable(tableName).build()); + + RowChangeEvent rowChangeEvent = new RowChangeEvent(rowBuilder.build()); + rowChangeEvent.setTimeStamp(timeStamp); + + // deleteData + RetinaProto.DeleteData.Builder deleteDataBuilder = RetinaProto.DeleteData.newBuilder() + .addIndexKeys(rowChangeEvent.getBeforeKey()); + tableUpdateDataBuilder.addDeleteData(deleteDataBuilder.build()); + + // insertData + RetinaProto.InsertData.Builder insertDataBuilder = RetinaProto.InsertData.newBuilder() + .addColValues(ByteString.copyFromUtf8(Integer.toString(accountID))) + .addColValues(ByteString.copyFromUtf8(Integer.toString(userID))) + .addColValues(ByteString.copyFromUtf8(Float.toString(newBalance))) + .addColValues(ByteString.copyFromUtf8(Integer.toString(isBlocked))) + .addColValues(ByteString.copyFromUtf8(DateUtil.convertDebeziumTimestampToString(newTs))) + .addIndexKeys(rowChangeEvent.getAfterKey()); + tableUpdateDataBuilder.addInsertData(insertDataBuilder.build()); + } + + tableUpdateData.add(tableUpdateDataBuilder.build()); + + long startTime = System.currentTimeMillis(); + if (!writer.writeTrans(schemaName, tableUpdateData)) + { + logger.error("Error Write Trans"); + System.exit(-1); + } + long endTime = System.currentTimeMillis(); + logger.debug("writeTrans batch " + batchIndex + " took " + (endTime - startTime) + " ms"); + + // commit transaction + transService.commitTrans(ctx.getTransId(), false); + + } catch (Exception e) + { + throw new RuntimeException(e); + } + }, clientExecutor); + + futures.add(batchFuture); + } + + // 等待所有 batch 完成 + for (CompletableFuture f : futures) + { + f.get(); + } + + long end = System.currentTimeMillis(); + double seconds = (end - start) / 1000.0; + double updatesPerSec = totalUpdates / seconds; + double transPerSec = batchCount / seconds; + logger.info("Updated " + totalUpdates + " rows in " + seconds + "s, rate=" + updatesPerSec + " updates/s, " + transPerSec + " trans/s"); + + clientExecutor.shutdown(); + } + + + @Test + public void testInsertTwoTablePerformance() throws + RetinaException, SinkException, TransException, IOException, ExecutionException, InterruptedException + { + String schemaName = "pixels_bench_sf1x"; + String tableName = "checkingaccount"; + String tableName2 = "savingaccount"; + PixelsSinkWriter writer = PixelsSinkWriterFactory.getWriter(); + + TransactionProxy manager = TransactionProxy.Instance(); + // Step 1: Insert 10,000 records + int totalInserts = retinaPerformanceTestRowCount; + int batchSize = 50; + int batchCount = totalInserts / batchSize; + + + long start = System.currentTimeMillis(); + + List> futures = new ArrayList<>(); + + for (int b = 0; b < batchCount; b++) + { + TransContext ctx = manager.getNewTransContext(); + long timeStamp = ctx.getTimestamp(); + + List tableUpdateData = new ArrayList<>(); + RetinaProto.TableUpdateData.Builder tableUpdateDataBuilder = + RetinaProto.TableUpdateData.newBuilder() + .setTableName(tableName) + .setPrimaryIndexId(metadataRegistry.getPrimaryIndexKeyId(schemaName, tableName)); + RetinaProto.TableUpdateData.Builder tableUpdateDataBuilder2 = + RetinaProto.TableUpdateData.newBuilder() + .setTableName(tableName2) + .setPrimaryIndexId(metadataRegistry.getPrimaryIndexKeyId(schemaName, tableName2)); + for (int i = 0; i < batchSize; i++) + { + int accountID = b * batchSize + i; + int userID = accountID % 1000; + float balance = 1000.0f + accountID; + int isBlocked = 0; + long ts = System.currentTimeMillis(); + + byte[][] cols = new byte[5][]; + cols[0] = Integer.toString(accountID).getBytes(StandardCharsets.UTF_8); + cols[1] = Integer.toString(userID).getBytes(StandardCharsets.UTF_8); + cols[2] = Float.toString(balance).getBytes(StandardCharsets.UTF_8); + cols[3] = Integer.toString(isBlocked).getBytes(StandardCharsets.UTF_8); + cols[4] = DateUtil.convertDebeziumTimestampToString(ts).getBytes(StandardCharsets.UTF_8); + // cols[4] = Long.toString(ts).getBytes(StandardCharsets.UTF_8); + // after row + SinkProto.RowValue.Builder afterValueBuilder = SinkProto.RowValue.newBuilder() + .addValues(SinkProto.ColumnValue.newBuilder().setValue(ByteString.copyFrom(cols[0])).build()) + .addValues(SinkProto.ColumnValue.newBuilder().setValue(ByteString.copyFrom(cols[1])).build()) + .addValues(SinkProto.ColumnValue.newBuilder().setValue(ByteString.copyFrom(cols[2])).build()) + .addValues(SinkProto.ColumnValue.newBuilder().setValue(ByteString.copyFrom(cols[3])).build()) + .addValues(SinkProto.ColumnValue.newBuilder().setValue(ByteString.copyFrom(cols[4])).build()); + + // RowRecord + SinkProto.RowRecord.Builder rowBuilder = SinkProto.RowRecord.newBuilder() + .setOp(SinkProto.OperationType.INSERT) + .setAfter(afterValueBuilder) + .setSource( + SinkProto.SourceInfo.newBuilder() + .setDb(schemaName) + .setTable(tableName) + .build() + ); + SinkProto.RowRecord.Builder rowBuilder2 = SinkProto.RowRecord.newBuilder() + .setOp(SinkProto.OperationType.INSERT) + .setAfter(afterValueBuilder) + .setSource( + SinkProto.SourceInfo.newBuilder() + .setDb(schemaName) + .setTable(tableName2) + .build() + ); + RowChangeEvent rowChangeEvent = new RowChangeEvent(rowBuilder.build()); + rowChangeEvent.setTimeStamp(timeStamp); + RowChangeEvent rowChangeEvent2 = new RowChangeEvent(rowBuilder.build()); + rowChangeEvent2.setTimeStamp(timeStamp); + // InsertData + RetinaProto.InsertData.Builder insertDataBuilder = RetinaProto.InsertData.newBuilder() + .addColValues(ByteString.copyFrom(cols[0])) + .addColValues(ByteString.copyFrom(cols[1])) + .addColValues(ByteString.copyFrom(cols[2])) + .addColValues(ByteString.copyFrom(cols[3])) + .addColValues(ByteString.copyFrom(cols[4])) + .addIndexKeys(rowChangeEvent.getAfterKey()); + RetinaProto.InsertData.Builder insertDataBuilder2 = RetinaProto.InsertData.newBuilder() + .addColValues(ByteString.copyFrom(cols[0])) + .addColValues(ByteString.copyFrom(cols[1])) + .addColValues(ByteString.copyFrom(cols[2])) + .addColValues(ByteString.copyFrom(cols[3])) + .addColValues(ByteString.copyFrom(cols[4])) + .addIndexKeys(rowChangeEvent2.getAfterKey()); + tableUpdateDataBuilder.addInsertData(insertDataBuilder.build()); + tableUpdateDataBuilder2.addInsertData(insertDataBuilder2.build()); + } + + tableUpdateData.add(tableUpdateDataBuilder.build()); + tableUpdateData.add(tableUpdateDataBuilder2.build()); + // retinaService.updateRecord(schemaName, tableUpdateData, timeStamp); + long startTime = System.currentTimeMillis(); // 使用 nanoTime 获取更精确的时间 + + + CompletableFuture future = CompletableFuture.runAsync(() -> + { + try + { + // 执行原始的 writeTrans 方法 + // writer.writeTrans(schemaName, tableUpdateData, timeStamp); + // 记录结束时间 + long endTime = System.currentTimeMillis(); + + // 计算并输出耗时(单位:毫秒) + long duration = endTime - startTime; + logger.debug("writeTrans took " + duration + " milliseconds"); + transService.commitTrans(ctx.getTransId(), false); + } catch (TransException e) + { + e.printStackTrace(); + throw new RuntimeException(e); + } + + }, executor); + + futures.add(future); + } + + for (CompletableFuture future : futures) + { + future.get(); + } + + long end = System.currentTimeMillis(); + double seconds = (end - start) / 1000.0; + double insertsPerSec = totalInserts * 2 / seconds; + double transPerSec = batchCount * 2 / seconds; + logger.info("Inserted " + totalInserts + " rows in " + seconds + "s, rate=" + insertsPerSec + " inserts/s," + transPerSec + "trans/s"); + } +} diff --git a/src/test/java/io/pixelsdb/pixels/sink/writer/TpcHTest.java b/src/test/java/io/pixelsdb/pixels/sink/writer/TpcHTest.java new file mode 100644 index 0000000..a3edbca --- /dev/null +++ b/src/test/java/io/pixelsdb/pixels/sink/writer/TpcHTest.java @@ -0,0 +1,359 @@ +/* + * 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.writer; + +import io.pixelsdb.pixels.common.exception.RetinaException; +import io.pixelsdb.pixels.common.exception.TransException; +import io.pixelsdb.pixels.common.metadata.MetadataService; +import io.pixelsdb.pixels.common.retina.RetinaService; +import io.pixelsdb.pixels.common.transaction.TransContext; +import io.pixelsdb.pixels.common.transaction.TransService; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.nio.charset.StandardCharsets; +import java.util.logging.Logger; + +public class TpcHTest +{ + + static Logger logger = Logger.getLogger(TpcHTest.class.getName()); + static RetinaService retinaService; + static MetadataService metadataService; + static TransService transService; + static String schemaName = "pixels_tpch"; + TransContext transContext; + + @BeforeAll + public static void setUp() + { + retinaService = RetinaService.Instance(); + metadataService = MetadataService.Instance(); + transService = TransService.Instance(); + } + + @BeforeEach + public void getTrans() throws TransException + { + transContext = transService.beginTrans(false); + } + + @AfterEach + public void commitTrans() throws TransException + { + transService.commitTrans(transContext.getTransId(), false); + } + + + @Test + public void insertCustomer() throws RetinaException + { + /* + Column | Type | Extra | Comment + --------------+---------------+-------+--------- + c_custkey | bigint | | + c_name | varchar(25) | | + c_address | varchar(40) | | + c_nationkey | bigint | | + c_phone | char(15) | | + c_acctbal | decimal(15,2) | | + c_mktsegment | char(10) | | + c_comment | varchar(117) | | + (8 rows) + */ + String tableName = "customer"; + for (int i = 0; i < 10; ++i) + { + byte[][] cols = new byte[8][]; + + cols[0] = Long.toString(i).getBytes(StandardCharsets.UTF_8); // c_custkey + cols[1] = ("Customer_" + i).getBytes(StandardCharsets.UTF_8); // c_name + cols[2] = ("Address_" + i).getBytes(StandardCharsets.UTF_8); // c_address + cols[3] = Long.toString(i % 5).getBytes(StandardCharsets.UTF_8); // c_nationkey + cols[4] = String.format("123-456-789%02d", i).getBytes(StandardCharsets.UTF_8); // c_phone (char(15)) + cols[5] = String.format("%.2f", i * 1000.0).getBytes(StandardCharsets.UTF_8); // c_acctbal (decimal) + cols[6] = ("SEGMENT" + (i % 3)).getBytes(StandardCharsets.UTF_8); // c_mktsegment (char(10)) + cols[7] = ("This is customer " + i).getBytes(StandardCharsets.UTF_8); // c_comment + +// boolean result = retinaService.insertRecord(schemaName, tableName, cols, transContext.getTimestamp()); +// logger.info("Inserted customer #" + i + " → result: " + result); +// Assertions.assertTrue(result); + } + } + + @Test + public void insertRegion() throws RetinaException + { + /* + Column | Type | Extra | Comment + ------------+--------------+-------+--------- + r_regionkey | bigint | | + r_name | char(25) | | + r_comment | varchar(152) | | + (3 rows) + */ + String tableName = "region"; + int start = 5; + int end = 10; + for (int i = start; i < end; ++i) + { + byte[][] cols = new byte[3][]; + + cols[0] = Long.toString(i).getBytes(StandardCharsets.UTF_8); // r_regionkey + cols[1] = String.format("Region_%02d", i).getBytes(StandardCharsets.UTF_8); // r_name (char(25)) + cols[2] = ("This is region number " + i).getBytes(StandardCharsets.UTF_8); // r_comment + +// boolean result = retinaService.insertRecord(schemaName, tableName, cols, transContext.getTimestamp()); +// logger.info("Inserted region #" + i + " → result: " + result); +// Assertions.assertTrue(result); + } + } + + @Test + public void insertNation() throws RetinaException + { + /* + Column | Type | Extra | Comment + ------------+--------------+-------+--------- + n_nationkey | bigint | | + n_name | char(25) | | + n_regionkey | bigint | | + n_comment | varchar(152) | | + */ + String tableName = "nation"; + int start = 0; + int end = 10; + + for (int i = start; i < end; ++i) + { + byte[][] cols = new byte[4][]; + + cols[0] = Long.toString(i).getBytes(StandardCharsets.UTF_8); // n_nationkey + cols[1] = String.format("Nation_%02d", i).getBytes(StandardCharsets.UTF_8); // n_name (char(25)) + cols[2] = Long.toString(i % 5).getBytes(StandardCharsets.UTF_8); // n_regionkey + cols[3] = ("This is nation number " + i).getBytes(StandardCharsets.UTF_8); // n_comment + +// boolean result = retinaService.insertRecord(schemaName, tableName, cols, transContext.getTimestamp()); +// logger.info("Inserted nation #" + i + " → result: " + result); +// Assertions.assertTrue(result); + } + } + + @Test + public void insertLineItem() throws RetinaException + { + /* + * Table: lineitem + * Columns: + * l_orderkey | bigint + * l_partkey | bigint + * l_suppkey | bigint + * l_linenumber | integer + * l_quantity | decimal(15,2) + * l_extendedprice | decimal(15,2) + * l_discount | decimal(15,2) + * l_tax | decimal(15,2) + * l_returnflag | char(1) + * l_linestatus | char(1) + * l_shipdate | date + * l_commitdate | date + * l_receiptdate | date + * l_shipinstruct | char(25) + * l_shipmode | char(10) + * l_comment | varchar(44) + */ + String tableName = "lineitem"; + int recordCount = 10; + + for (int i = 0; i < recordCount; ++i) + { + byte[][] cols = new byte[16][]; + + cols[0] = Long.toString(1000 + i).getBytes(StandardCharsets.UTF_8); // l_orderkey + cols[1] = Long.toString(2000 + i).getBytes(StandardCharsets.UTF_8); // l_partkey + cols[2] = Long.toString(3000 + i).getBytes(StandardCharsets.UTF_8); // l_suppkey + cols[3] = Integer.toString(i + 1).getBytes(StandardCharsets.UTF_8); // l_linenumber + cols[4] = String.format("%.2f", 10.0 + i).getBytes(StandardCharsets.UTF_8); // l_quantity + cols[5] = String.format("%.2f", 100.0 + i * 10).getBytes(StandardCharsets.UTF_8); // l_extendedprice + cols[6] = String.format("%.2f", 0.05).getBytes(StandardCharsets.UTF_8); // l_discount + cols[7] = String.format("%.2f", 0.08).getBytes(StandardCharsets.UTF_8); // l_tax + cols[8] = "R".getBytes(StandardCharsets.UTF_8); // l_returnflag + cols[9] = "O".getBytes(StandardCharsets.UTF_8); // l_linestatus + cols[10] = "2025-07-10".getBytes(StandardCharsets.UTF_8); // l_shipdate + cols[11] = "2025-07-12".getBytes(StandardCharsets.UTF_8); // l_commitdate + cols[12] = "2025-07-15".getBytes(StandardCharsets.UTF_8); // l_receiptdate + cols[13] = String.format("DELIVER TO %d", i).getBytes(StandardCharsets.UTF_8); // l_shipinstruct + cols[14] = "AIR".getBytes(StandardCharsets.UTF_8); // l_shipmode + cols[15] = String.format("Order comment %d", i).getBytes(StandardCharsets.UTF_8); // l_comment + +// boolean result = retinaService.insertRecord(schemaName, tableName, cols, transContext.getTimestamp()); +// logger.info("Inserted lineitem #" + i + " → result: " + result); +// Assertions.assertTrue(result); + } + } + + @Test + public void insertOrders() throws RetinaException + { + /* + * Table: orders + * Columns: + * o_orderkey | bigint + * o_custkey | bigint + * o_orderstatus | char(1) + * o_totalprice | decimal(15,2) + * o_orderdate | date + * o_orderpriority | char(15) + * o_clerk | char(15) + * o_shippriority | integer + * o_comment | varchar(79) + */ + String tableName = "orders"; + int recordCount = 10; + + for (int i = 0; i < recordCount; ++i) + { + byte[][] cols = new byte[9][]; + + cols[0] = Long.toString(10000 + i).getBytes(StandardCharsets.UTF_8); // o_orderkey + cols[1] = Long.toString(500 + i).getBytes(StandardCharsets.UTF_8); // o_custkey + cols[2] = "O".getBytes(StandardCharsets.UTF_8); // o_orderstatus + cols[3] = String.format("%.2f", 1234.56 + i * 10).getBytes(StandardCharsets.UTF_8); // o_totalprice + cols[4] = String.format("2025-07-%02d", 10 + i).getBytes(StandardCharsets.UTF_8); // o_orderdate + cols[5] = String.format("PRIORITY-%d", i % 5).getBytes(StandardCharsets.UTF_8); // o_orderpriority + cols[6] = String.format("Clerk#%03d", i).getBytes(StandardCharsets.UTF_8); // o_clerk + cols[7] = Integer.toString(i % 10).getBytes(StandardCharsets.UTF_8); // o_shippriority + cols[8] = String.format("This is order %d", i).getBytes(StandardCharsets.UTF_8); // o_comment + +// boolean result = retinaService.insertRecord(schemaName, tableName, cols, transContext.getTimestamp()); +// logger.info("Inserted order #" + i + " → result: " + result); +// Assertions.assertTrue(result); + } + } + + @Test + public void insertPart() throws RetinaException + { + /* + * Table: part + * Columns: + * p_partkey | bigint + * p_name | varchar(55) + * p_mfgr | char(25) + * p_brand | char(10) + * p_type | varchar(25) + * p_size | integer + * p_container | char(10) + * p_retailprice | decimal(15,2) + * p_comment | varchar(23) + */ + String tableName = "part"; + int recordCount = 10; + + for (int i = 0; i < recordCount; ++i) + { + byte[][] cols = new byte[9][]; + + cols[0] = Long.toString(2000 + i).getBytes(StandardCharsets.UTF_8); // p_partkey + cols[1] = ("PartName_" + i).getBytes(StandardCharsets.UTF_8); // p_name + cols[2] = String.format("MFGR#%02d", i % 5).getBytes(StandardCharsets.UTF_8); // p_mfgr + cols[3] = String.format("Brand#%d", i % 3).getBytes(StandardCharsets.UTF_8); // p_brand + cols[4] = ("TYPE_" + (i % 4)).getBytes(StandardCharsets.UTF_8); // p_type + cols[5] = Integer.toString(5 + i).getBytes(StandardCharsets.UTF_8); // p_size + cols[6] = ("Box" + (i % 6)).getBytes(StandardCharsets.UTF_8); // p_container + cols[7] = String.format("%.2f", 99.99 + i * 2.5).getBytes(StandardCharsets.UTF_8); // p_retailprice + cols[8] = ("Comment_" + i).getBytes(StandardCharsets.UTF_8); // p_comment + +// boolean result = retinaService.insertRecord(schemaName, tableName, cols, transContext.getTimestamp()); +// logger.info("Inserted part #" + i + " → result: " + result); +// Assertions.assertTrue(result); + } + } + + @Test + public void insertPartSupp() throws RetinaException + { + /* + * Table: partsupp + * Columns: + * ps_partkey | bigint + * ps_suppkey | bigint + * ps_availqty | integer + * ps_supplycost | decimal(15,2) + * ps_comment | varchar(199) + */ + String tableName = "partsupp"; + int recordCount = 10; + + for (int i = 0; i < recordCount; ++i) + { + byte[][] cols = new byte[5][]; + + cols[0] = Long.toString(1000 + i).getBytes(StandardCharsets.UTF_8); // ps_partkey + cols[1] = Long.toString(500 + i).getBytes(StandardCharsets.UTF_8); // ps_suppkey + cols[2] = Integer.toString(300 + i * 10).getBytes(StandardCharsets.UTF_8); // ps_availqty + cols[3] = String.format("%.2f", 50.0 + i * 5.5).getBytes(StandardCharsets.UTF_8); // ps_supplycost + cols[4] = ("This is supplier comment #" + i).getBytes(StandardCharsets.UTF_8); // ps_comment + +// boolean result = retinaService.insertRecord(schemaName, tableName, cols, transContext.getTimestamp()); +// logger.info("Inserted partsupp #" + i + " → result: " + result); +// Assertions.assertTrue(result); + } + } + + @Test + public void insertSupplier() throws RetinaException + { + /* + * Table: supplier + * Columns: + * s_suppkey | bigint + * s_name | char(25) + * s_address | varchar(40) + * s_nationkey | bigint + * s_phone | char(15) + * s_acctbal | decimal(15,2) + * s_comment | varchar(101) + */ + String tableName = "supplier"; + int recordCount = 10; + + for (int i = 0; i < recordCount; ++i) + { + byte[][] cols = new byte[7][]; + + cols[0] = Long.toString(2000 + i).getBytes(StandardCharsets.UTF_8); // s_suppkey + cols[1] = String.format("Supplier_%02d", i).getBytes(StandardCharsets.UTF_8); // s_name + cols[2] = ("Address_" + i).getBytes(StandardCharsets.UTF_8); // s_address + cols[3] = Long.toString(i % 5).getBytes(StandardCharsets.UTF_8); // s_nationkey + cols[4] = String.format("987-654-32%03d", i).getBytes(StandardCharsets.UTF_8); // s_phone + cols[5] = String.format("%.2f", 5000.0 + i * 123.45).getBytes(StandardCharsets.UTF_8); // s_acctbal + cols[6] = ("Supplier comment for ID " + i).getBytes(StandardCharsets.UTF_8); // s_comment + +// boolean result = retinaService.insertRecord(schemaName, tableName, cols, transContext.getTimestamp()); +// logger.info("Inserted supplier #" + i + " → result: " + result); +// Assertions.assertTrue(result); + } + } + +} diff --git a/src/test/java/io/pixelsdb/pixels/sink/writer/retina/TableWriterProxyTest.java b/src/test/java/io/pixelsdb/pixels/sink/writer/retina/TableWriterProxyTest.java new file mode 100644 index 0000000..169c94b --- /dev/null +++ b/src/test/java/io/pixelsdb/pixels/sink/writer/retina/TableWriterProxyTest.java @@ -0,0 +1,55 @@ +/* + * 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.writer.retina; + +import io.pixelsdb.pixels.sink.config.factory.PixelsSinkConfigFactory; +import io.pixelsdb.pixels.sink.event.RowChangeEventTest; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; + +public class TableWriterProxyTest +{ + private static final Logger LOGGER = LoggerFactory.getLogger(TableWriterProxyTest.class); + + + String tableName = "test"; + + @BeforeAll + public static void init() throws IOException + { + PixelsSinkConfigFactory.initialize("/home/ubuntu/pixels-sink/conf/pixels-sink.aws.properties"); + } + + @Test + public void testGetSameTableWriter() throws IOException + { + TableWriterProxy tableWriterProxy = TableWriterProxy.getInstance(); + + for(int i = 0; i < 10 ; i++) + { + TableWriter tableWriter = tableWriterProxy.getTableWriter(tableName, 0, 0); + } + } +} diff --git a/src/test/resources/log4j2.properties b/src/test/resources/log4j2.properties index e91406a..09ab7d9 100644 --- a/src/test/resources/log4j2.properties +++ b/src/test/resources/log4j2.properties @@ -1,7 +1,7 @@ status=info name=pixels-sink filter.threshold.type=ThresholdFilter -filter.threshold.level=debug +filter.threshold.level=info appender.console.type=Console appender.console.name=STDOUT appender.console.layout.type=PatternLayout @@ -12,6 +12,16 @@ appender.rolling.append=true appender.rolling.fileName=${env:PIXELS_HOME}/logs/pixels-sink.log appender.rolling.layout.type=PatternLayout appender.rolling.layout.pattern=%-d{yyyy-MM-dd HH:mm:ss,SSS} [%c]-[%p] %m%n -rootLogger.level=debug +rootLogger.level=info rootLogger.appenderRef.stdout.ref=STDOUT rootLogger.appenderRef.log.ref=log +logger.transaction.name=io.pixelsdb.pixels.sink.sink.retina.RetinaWriter +logger.transaction.level=info +logger.transaction.appenderRef.log.ref=log +logger.transaction.appenderRef.stdout.ref=STDOUT +logger.transaction.additivity=false +logger.grpc.name=io.grpc.netty.shaded.io.grpc.netty.NettyClientHandler +logger.grpc.level=info +logger.grpc.additivity=false +logger.grpc.appenderRef.log.ref=log +logger.grpc.appenderRef.stdout.ref=STDOUT