luchunliang opened a new pull request, #12192:
URL: https://github.com/apache/inlong/pull/12192

   Fixes #12191 
   
   ### Motivation
   
   - **On-the-fly re-encoding** — decode an incoming line (CSV / KV / PB / 
JSON), apply SQL projection / filter / function calls (e.g. 
`STR_TO_MAP(URL_DECODE(event_value), '&', '=')['HY50']`), then emit CSV / KV / 
JSON to Pulsar.
   - **Row explosion** — transforms like `$childIndex` + child-array unpacking 
can turn one event into multiple downstream messages; the sink pipeline must 
support that.
   - **Filtering** — `WHERE` clauses can filter events entirely; the sink must 
ack the event without emitting anything, without leaking transactions.
   - **Consistency with Kafka side** — reduce operational surprise for users: 
same `transformSql`, same encoding switch, same filter/explode semantics 
regardless of the underlying MQ.
   
   ### Modifications
   
   ### Proposed change
   
   Four files changed. All changes are additive; existing Pulsar deployments 
without `transformSql` are unaffected.
   
   #### 1) `inlong-common` : `PulsarSinkConfig`
   
   Add encoding hints and defaults (mirroring `KafkaSinkConfig`):
   
   ```java
   public static final String MESSAGE_TYPE_CSV = "csv";
   public static final Character CSV_DEFAULT_DELIMITER = '|';
   public static final String MESSAGE_TYPE_KV = "kv";
   public static final Character KV_DEFAULT_ENTRYSPLITTER = '&';
   public static final Character KV_DEFAULT_KVSPLITTER = '=';
   public static final String MESSAGE_TYPE_JSON = "json";
   
   private String messageType;
   private Character delimiter;
   private Character escapeChar;
   private Character entrySplitter;
   private Character kvSplitter;
   ```
   
   The pre-existing fields (`pulsarTenant / namespace / topic / partitionNum`) 
are kept intact.
   
   #### 2) `PulsarIdConfig`
   
   Add `dataFlowId` (aligned with `KafkaIdConfig`) so the handler can look up 
the correct `TransformProcessor`:
   
   - `Map<String,String>` constructor initializes `dataFlowId = uid` 
(back-compat).
   - `create(DataFlowConfig)` builder sets `dataFlowId = 
dataFlowConfig.getDataflowId()`.
   
   #### 3) `PulsarFederationSinkContext`
   
   Introduce transform caching and encoder wiring:
   
   ```java
   // Map<threadId, Map<dataFlowId, TransformProcessor>>
   protected Map<Long, Map<String, TransformProcessor<String, ?>>> transformMap 
= new ConcurrentHashMap<>();
   
   public TransformProcessor<String, ?> getTransformProcessor(String 
dataFlowId) { ... }
   private Map<String, TransformProcessor<String, ?>> 
reloadTransform(TaskConfig taskConfig) { ... }
   private TransformProcessor<String, ?> createTransform(DataFlowConfig 
dataFlowConfig) { ... }
   private SinkEncoder<?> createSinkEncoder(SinkConfig sinkConfig) { ... }
   ```
   
   Semantics:
   
   - `reload()` uses `taskConfigJson` / `sortTaskConfigJson` for change 
detection (via `replaceConfig(...)` in the base `SinkContext`).
   - When `unifiedConfiguration` is on and config changes, 
`transformMap.clear()` so stale processors on worker threads are dropped.
   - `createTransform` builds a `TransformProcessor` using the base class 
helpers `createTransformConfig` + `createSourceDecoder` + our new 
`createSinkEncoder`.
   - `createSinkEncoder` dispatches on `PulsarSinkConfig.messageType`:
     - `csv` → `CsvSinkInfo(encodingType, delimiter [default '|'], escapeChar, 
fieldInfos)`
     - `kv` → `KvSinkInfo(encodingType, fieldInfos)` + `entrySplitter [default 
'&']` + `kvSplitter [default '=']`
     - `json` → `MapSinkInfo(encodingType, fieldInfos)`
     - otherwise → default CSV encoder with `'|'`.
   
   Flows without `transformSql` skip `TransformProcessor` construction entirely.
   
   #### 4) `IEvent2PulsarRecordHandler` (**breaking** — see notes)
   
   Signature updated to align with the Kafka handler and to allow 0/1/N outputs:
   
   ```java
   public interface IEvent2PulsarRecordHandler {
       List<byte[]> parse(PulsarFederationSinkContext context, ProfileEvent 
event, PulsarIdConfig idConfig)
               throws IOException;
   }
   ```
   
   #### 5) `DefaultEvent2PulsarRecordHandler`
   
   Two branches, matching the Kafka side 1:1:
   
   ```java
   @Override
   public List<byte[]> parse(PulsarFederationSinkContext context, ProfileEvent 
event, PulsarIdConfig idConfig)
           throws IOException {
       TransformProcessor<String, ?> processor = 
context.getTransformProcessor(idConfig.getDataFlowId());
       if (processor != null) {
           return parseByTransform(context, event, processor);
       }
       return Arrays.asList(parseByBytes(event, idConfig));
   }
   ```
   
   - **`parseByTransform`** — builds `extParams` from 
`context.getSinkContext().getParameters()` + `event.getHeaders()`, calls 
`processor.transformForBytes(event.getBody(), extParams)`, converts each result:
     - `String`  → `getBytes()`
     - `byte[]`  → as-is
     - other     → `gson.toJson(...).getBytes()`
   - **`parseByBytes`** — the original behavior is preserved: for `TEXT` 
prepend `ftime + separator + extinfo + separator`, then append 
`event.getBody()`; for `PB / JCE / UNKNOWN` just emit `event.getBody()`.
   
   #### 6) `PulsarProducerCluster#send`
   
   Adapted to the new `List<byte[]>` contract while keeping the "one 
transaction per event" model:
   
   - Resolve `PulsarIdConfig` from `event.getUid()`.
   - Call `handler.parse(sinkContext, event, idConfig)`.
   - If empty / null → `tx.commit(); event.ack(); tx.close();` (filter case).
   - Otherwise send N messages in parallel and aggregate via `AtomicInteger 
remaining` + `AtomicBoolean failed`:
     - Every `sendAsync` callback records a per-message metric.
     - When the last message's callback fires: if any failed → `tx.rollback()`, 
else `tx.commit()` + `event.ack()`; finally `tx.close()`.
   
   This preserves atomicity — an event either fully lands or fully rolls back — 
even when it fans out to multiple Pulsar messages.
   
   ### Behavior matrix
   
   | Config | `transformSql` present? | `messageType` | Result |
   | --- | --- | --- | --- |
   | Pulsar sink (existing users) | ❌ | any / unset | Same as today: 
`parseByBytes` → 1 message per event |
   | Pulsar sink | ✅ | `csv` | Transform → CSV encoder (custom delimiter / 
escape) → N messages |
   | Pulsar sink | ✅ | `kv`  | Transform → KV encoder (custom entry / kv 
splitter) → N messages |
   | Pulsar sink | ✅ | `json` | Transform → MAP (JSON) encoder → N messages |
   | Pulsar sink | ✅ | unset / unknown | Transform → default CSV encoder with 
`'|'` |
   
   ### Backward compatibility
   
   - **Wire config**: `PulsarSinkConfig` only adds fields/constants; old JSON 
that doesn't set `messageType / delimiter / escapeChar / entrySplitter / 
kvSplitter` deserializes fine.
   - **Runtime behavior for existing flows**: when `transformSql` is empty, 
`TransformProcessor` is not built, `parseByTransform` is not taken, 
`parseByBytes` returns exactly one payload — semantically identical to today.
   - **API break — internal SPI only**: `IEvent2PulsarRecordHandler#parse` 
return type changes from `byte[]` to `List<byte[]>` and gains `PulsarIdConfig`. 
This interface is a sort-standalone internal SPI (not published to 
`inlong-common`), and the only known implementation 
`DefaultEvent2PulsarRecordHandler` is updated in the same change. Users who 
plugged in a custom handler via the `eventHandler` common property will need a 
small adaptation:
     ```java
     // before
     byte[] out = doStuff(event);
     // after
     return Arrays.asList(doStuff(event));
     ```
   - No public API on `SinkContext` / `PulsarFederationSinkContext` is removed 
or renamed; only additions.
   
   ### Risks / Notes
   
   - **Custom `eventHandler` implementations must adapt** to the new interface 
signature. Because a wrong signature will surface as a `NoSuchMethodError` at 
boot, this failure is loud (not silent).
   - **Per-message vs per-event metrics**: for a fanout event, 
`addSendResultMetric` is now invoked once per outbound message, which slightly 
changes metric cardinality (more accurate, but different from previous 
behavior). Existing dashboards that count "sink send success = event count" may 
need to switch to "sink send success = message count".
   - **Transactional atomicity in fanout**: if any of the N sub-sends fails, 
the whole transaction rolls back (all N messages are considered unsent from the 
sort perspective, even the ones the broker already acked). This mirrors the 
current single-message semantics and keeps ack behavior predictable at the cost 
of possible duplicate delivery under partial failures — same trade-off the 
Kafka side already takes.
   - **JSON encoding path** uses `MapSinkInfo` 
(`SinkEncoderFactory.createMapEncoder`) to keep parity with Kafka. If you need 
strict record-JSON in the future (schema-aware), that is a separate follow-up.
   
   ### Files changed
   
   - 
`inlong-common/src/main/java/org/apache/inlong/common/pojo/sort/dataflow/sink/PulsarSinkConfig.java`
   - 
`inlong-sort-standalone/sort-standalone-source/src/main/java/org/apache/inlong/sort/standalone/sink/pulsar/PulsarIdConfig.java`
   - 
`inlong-sort-standalone/sort-standalone-source/src/main/java/org/apache/inlong/sort/standalone/sink/pulsar/PulsarFederationSinkContext.java`
   - 
`inlong-sort-standalone/sort-standalone-source/src/main/java/org/apache/inlong/sort/standalone/sink/pulsar/IEvent2PulsarRecordHandler.java`
   - 
`inlong-sort-standalone/sort-standalone-source/src/main/java/org/apache/inlong/sort/standalone/sink/pulsar/DefaultEvent2PulsarRecordHandler.java`
   - 
`inlong-sort-standalone/sort-standalone-source/src/main/java/org/apache/inlong/sort/standalone/sink/pulsar/PulsarProducerCluster.java`
   
   ### Checklist
   
   - [x] Behavior of existing Pulsar sink pipelines (no `transformSql`) is 
preserved
   - [x] `PulsarSinkConfig` is JSON back-compat (only new fields/constants 
added)
   - [x] `PulsarFederationSinkContext` caches `TransformProcessor` per worker 
thread and clears it on config change
   - [x] `DefaultEvent2PulsarRecordHandler` supports 0 / 1 / N output rows
   - [x] `PulsarProducerCluster` sends N messages under one transaction and 
commits/rolls back atomically
   - [x] Parity with `KafkaFederationSinkContext` + 
`DefaultEvent2KafkaRecordHandler` on the transform code path
   ### Verifying this change
   
   *(Please pick either of the following options)*
   
   - [ ] This change is a trivial rework/code cleanup without any test coverage.
   
   - [ ] This change is already covered by existing tests, such as:
     *(please describe tests)*
   
   - [ ] This change added tests and can be verified as follows:
   
     *(example:)*
     - *Added integration tests for end-to-end deployment with large payloads 
(10MB)*
     - *Extended integration test for recovery after broker failure*
   
   ### Documentation
   
     - Does this pull request introduce a new feature? (yes / no)
     - If yes, how is the feature documented? (not applicable / docs / JavaDocs 
/ not documented)
     - If a feature is not applicable for documentation, explain why?
     - If a feature is not documented yet in this PR, please create a follow-up 
issue for adding the documentation
   


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to