Skip to content

Commit 6981e3f

Browse files
committed
feat: add support for collapsing sink batches based on message acknowledgment
- Introduced CollapseBatchOnMessageAck field in DataFlowSpec to control whether message acknowledgment forces MaxBatchSize=1. - Implemented logic in the processor and connectors to respect the new collapsing behavior. - Added unit tests to validate the new collapsing functionality for various scenarios. - Updated CRDs and documentation to reflect the new CollapseBatchOnMessageAck configuration.
1 parent 6e1b515 commit 6981e3f

17 files changed

Lines changed: 136 additions & 18 deletions

api/v1/dataflow_ack.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,3 +35,17 @@ func AckGranularityOrDefault(spec *DataFlowSpec) string {
3535
func AckGranularityIsMessage(spec *DataFlowSpec) bool {
3636
return AckGranularityOrDefault(spec) == AckGranularityMessage
3737
}
38+
39+
// CollapseBatchOnMessageAckOrDefault reports whether message-ack must force MaxBatchSize=1.
40+
// Default true preserves legacy coupling; false keeps sink batchSize with per-message source commit.
41+
func CollapseBatchOnMessageAckOrDefault(spec *DataFlowSpec) bool {
42+
if spec != nil && spec.CollapseBatchOnMessageAck != nil {
43+
return *spec.CollapseBatchOnMessageAck
44+
}
45+
return true
46+
}
47+
48+
// ShouldCollapseSinkBatch reports whether sinks must force MaxBatchSize=1 for the given spec.
49+
func ShouldCollapseSinkBatch(spec *DataFlowSpec) bool {
50+
return AckGranularityIsMessage(spec) && CollapseBatchOnMessageAckOrDefault(spec)
51+
}

api/v1/dataflow_ack_test.go

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,3 +38,33 @@ func TestAckGranularityIsMessage(t *testing.T) {
3838
assert.False(t, AckGranularityIsMessage(&DataFlowSpec{}))
3939
assert.True(t, AckGranularityIsMessage(&DataFlowSpec{AckGranularity: AckGranularityMessage}))
4040
}
41+
42+
func TestCollapseBatchOnMessageAckOrDefault(t *testing.T) {
43+
t.Parallel()
44+
45+
assert.True(t, CollapseBatchOnMessageAckOrDefault(nil))
46+
assert.True(t, CollapseBatchOnMessageAckOrDefault(&DataFlowSpec{}))
47+
48+
f := false
49+
assert.False(t, CollapseBatchOnMessageAckOrDefault(&DataFlowSpec{CollapseBatchOnMessageAck: &f}))
50+
tr := true
51+
assert.True(t, CollapseBatchOnMessageAckOrDefault(&DataFlowSpec{CollapseBatchOnMessageAck: &tr}))
52+
}
53+
54+
func TestShouldCollapseSinkBatch(t *testing.T) {
55+
t.Parallel()
56+
57+
assert.False(t, ShouldCollapseSinkBatch(nil))
58+
assert.False(t, ShouldCollapseSinkBatch(&DataFlowSpec{}))
59+
60+
f := false
61+
assert.True(t, ShouldCollapseSinkBatch(&DataFlowSpec{AckGranularity: AckGranularityMessage}))
62+
assert.False(t, ShouldCollapseSinkBatch(&DataFlowSpec{
63+
AckGranularity: AckGranularityMessage,
64+
CollapseBatchOnMessageAck: &f,
65+
}))
66+
assert.False(t, ShouldCollapseSinkBatch(&DataFlowSpec{
67+
AckGranularity: AckGranularityBatch,
68+
CollapseBatchOnMessageAck: &f,
69+
}))
70+
}

api/v1/dataflow_types.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,15 @@ type DataFlowSpec struct {
9696
// +optional
9797
AckGranularity string `json:"ackGranularity,omitempty"`
9898

99+
// CollapseBatchOnMessageAck forces sink MaxBatchSize=1 when ackGranularity is message.
100+
// Default: true (legacy coupling of ack semantics and write-batch size).
101+
// Set false to keep sink.config.batchSize while still committing source offsets
102+
// per message after a successful bulk write (watermark-style message ack).
103+
// Ignored when ackGranularity is batch.
104+
// +kubebuilder:default:=true
105+
// +optional
106+
CollapseBatchOnMessageAck *bool `json:"collapseBatchOnMessageAck,omitempty"`
107+
99108
// CheckpointReset clears persisted source checkpoint on the next processor start (one-shot).
100109
// Alternatively set annotation dataflow.dataflow.io/reset-checkpoint: "true" on the DataFlow.
101110
// +optional

api/v1/zz_generated.deepcopy.go

Lines changed: 5 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

config/crd/bases/dataflow.dataflow.io_dataflowcrons.yaml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -989,6 +989,15 @@ spec:
989989
Shrinks the duplicate window on pod crash from the debounce interval to roughly one batch.
990990
Default: false (debounced save only). Recommended for migration and cron workloads.
991991
type: boolean
992+
collapseBatchOnMessageAck:
993+
default: true
994+
description: |-
995+
CollapseBatchOnMessageAck forces sink MaxBatchSize=1 when ackGranularity is message.
996+
Default: true (legacy coupling of ack semantics and write-batch size).
997+
Set false to keep sink.config.batchSize while still committing source offsets
998+
per message after a successful bulk write (watermark-style message ack).
999+
Ignored when ackGranularity is batch.
1000+
type: boolean
9921001
concurrencyPolicy:
9931002
type: string
9941003
errors:

config/crd/bases/dataflow.dataflow.io_dataflows.yaml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -991,6 +991,15 @@ spec:
991991
Shrinks the duplicate window on pod crash from the debounce interval to roughly one batch.
992992
Default: false (debounced save only). Recommended for migration and cron workloads.
993993
type: boolean
994+
collapseBatchOnMessageAck:
995+
default: true
996+
description: |-
997+
CollapseBatchOnMessageAck forces sink MaxBatchSize=1 when ackGranularity is message.
998+
Default: true (legacy coupling of ack semantics and write-batch size).
999+
Set false to keep sink.config.batchSize while still committing source offsets
1000+
per message after a successful bulk write (watermark-style message ack).
1001+
Ignored when ackGranularity is batch.
1002+
type: boolean
9941003
errors:
9951004
description: Errors defines the error sink for messages that failed
9961005
to be written to the main sink

config/samples/kafka-to-clickhouse-high-volume.yaml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,9 @@ metadata:
55
spec:
66
# Высоконагруженный поток: увеличиваем буфер канала
77
channelBufferSize: 1000
8-
# Для high-volume потоков используем message-level ack
8+
# Message-level source commit without collapsing ClickHouse bulk inserts
99
ackGranularity: message
10+
collapseBatchOnMessageAck: false
1011
source:
1112
type: kafka
1213
config:

internal/connectors/base.go

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -173,10 +173,11 @@ func (c *connectorMetadata) SetMetadata(namespace, name string) {
173173

174174
// progressRecorder is an optional callback invoked after successful pipeline progress (e.g. sink flush + ack).
175175
type progressRecorder struct {
176-
onProgress func()
177-
batchAckSyncer checkpoint.BatchAckSyncer
178-
ackGranularity string
179-
checkpointReporter checkpointSaveReporter
176+
onProgress func()
177+
batchAckSyncer checkpoint.BatchAckSyncer
178+
ackGranularity string
179+
collapseBatchOnMessageAck *bool // nil → true (legacy) when message-ack
180+
checkpointReporter checkpointSaveReporter
180181
}
181182

182183
func (p *progressRecorder) setReporter(r checkpointSaveReporter) {
@@ -202,10 +203,27 @@ func (p *progressRecorder) SetAckGranularity(granularity string) {
202203
p.ackGranularity = v1.AckGranularityBatch
203204
}
204205

206+
// SetCollapseBatchOnMessageAck controls whether message-ack forces MaxBatchSize=1.
207+
// Wire from CRD via processor (default true when unset on the spec).
208+
func (p *progressRecorder) SetCollapseBatchOnMessageAck(collapse bool) {
209+
p.collapseBatchOnMessageAck = &collapse
210+
}
211+
205212
func (p *progressRecorder) ackGranularityIsMessage() bool {
206213
return p.ackGranularity == v1.AckGranularityMessage
207214
}
208215

216+
// shouldCollapseBatchForAck reports whether sink batching must be forced to 1.
217+
func (p *progressRecorder) shouldCollapseBatchForAck() bool {
218+
if !p.ackGranularityIsMessage() {
219+
return false
220+
}
221+
if p.collapseBatchOnMessageAck != nil {
222+
return *p.collapseBatchOnMessageAck
223+
}
224+
return true
225+
}
226+
209227
func (p *progressRecorder) notifyProgress() {
210228
if p.onProgress != nil {
211229
p.onProgress()

internal/connectors/base_progress_test.go

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,3 +150,22 @@ func TestAckAfterSuccessfulWrite_MessageGranularity(t *testing.T) {
150150
assert.Equal(t, int32(2), progressCalls.Load())
151151
assert.Equal(t, int32(2), syncer.calls.Load())
152152
}
153+
154+
func TestShouldCollapseBatchForAck(t *testing.T) {
155+
t.Parallel()
156+
157+
rec := &progressRecorder{}
158+
assert.False(t, rec.shouldCollapseBatchForAck())
159+
160+
rec.SetAckGranularity("message")
161+
assert.True(t, rec.shouldCollapseBatchForAck(), "default collapses when message-ack")
162+
163+
rec.SetCollapseBatchOnMessageAck(false)
164+
assert.False(t, rec.shouldCollapseBatchForAck())
165+
166+
rec.SetCollapseBatchOnMessageAck(true)
167+
assert.True(t, rec.shouldCollapseBatchForAck())
168+
169+
rec.SetAckGranularity("batch")
170+
assert.False(t, rec.shouldCollapseBatchForAck(), "batch ack never collapses via this helper")
171+
}

internal/connectors/batch_writer.go

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -65,9 +65,10 @@ func NewBatchWriteConfig(batchSize, flushIntervalSec *int32, defaultBatchSize in
6565
}
6666
}
6767

68-
// ApplyAckGranularity forces single-message batches when per-message ack is enabled.
69-
func ApplyAckGranularity(cfg BatchWriteConfig, messageAck bool) BatchWriteConfig {
70-
if messageAck {
68+
// ApplyAckGranularity forces single-message batches when collapseBatch is true
69+
// (typically message-ack with collapseBatchOnMessageAck enabled / default).
70+
func ApplyAckGranularity(cfg BatchWriteConfig, collapseBatch bool) BatchWriteConfig {
71+
if collapseBatch {
7172
cfg.MaxBatchSize = 1
7273
}
7374
return cfg

0 commit comments

Comments
 (0)