Skip to content

Commit 86dcfd3

Browse files
committed
Update probe
1 parent 1cb3fbd commit 86dcfd3

14 files changed

Lines changed: 270 additions & 29 deletions

cmd/processor/main.go

Lines changed: 18 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -146,19 +146,7 @@ func main() {
146146
})
147147
progressTimeout := progressTimeoutFromEnv(os.Getenv("PROCESSOR_PROGRESS_TIMEOUT_SECONDS"))
148148
mux.HandleFunc("/livez", func(w http.ResponseWriter, _ *http.Request) {
149-
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
150-
if !proc.Ready() {
151-
w.WriteHeader(http.StatusServiceUnavailable)
152-
_, _ = w.Write([]byte("not ready\n"))
153-
return
154-
}
155-
if proc.ProgressStale(progressTimeout) {
156-
w.WriteHeader(http.StatusServiceUnavailable)
157-
_, _ = w.Write([]byte("stale: no pipeline progress\n"))
158-
return
159-
}
160-
w.WriteHeader(http.StatusOK)
161-
_, _ = w.Write([]byte("ok\n"))
149+
writeLivez(w, proc.Ready, proc.ProgressStale, progressTimeout)
162150
})
163151
metricsServer := &http.Server{Addr: metricsPort, Handler: mux}
164152
go func() {
@@ -230,6 +218,23 @@ func main() {
230218
logger.Info("Processor stopped successfully")
231219
}
232220

221+
// writeLivez implements GET /livez for Kubernetes liveness probes.
222+
func writeLivez(w http.ResponseWriter, ready func() bool, progressStale func(time.Duration) bool, progressTimeout time.Duration) {
223+
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
224+
if !ready() {
225+
w.WriteHeader(http.StatusServiceUnavailable)
226+
_, _ = w.Write([]byte("not ready\n"))
227+
return
228+
}
229+
if progressStale(progressTimeout) {
230+
w.WriteHeader(http.StatusServiceUnavailable)
231+
_, _ = w.Write([]byte("stale: no pipeline progress\n"))
232+
return
233+
}
234+
w.WriteHeader(http.StatusOK)
235+
_, _ = w.Write([]byte("ok\n"))
236+
}
237+
233238
// processorLevelFromEnv returns zap LevelEnabler from LOG_LEVEL env if set, otherwise optsLevel.
234239
// progressTimeoutFromEnv parses PROCESSOR_PROGRESS_TIMEOUT_SECONDS (0 or unset disables stale checks).
235240
func progressTimeoutFromEnv(env string) time.Duration {

cmd/processor/main_test.go

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,10 @@ package main
33
import (
44
"context"
55
"errors"
6+
"net/http"
7+
"net/http/httptest"
68
"testing"
9+
"time"
710

811
"github.com/stretchr/testify/assert"
912
)
@@ -14,6 +17,41 @@ func TestIsRetryableProcessorError(t *testing.T) {
1417
assert.False(t, isRetryableProcessorError(errors.New("permission denied")))
1518
}
1619

20+
func TestWriteLivez(t *testing.T) {
21+
t.Parallel()
22+
23+
t.Run("not ready", func(t *testing.T) {
24+
t.Parallel()
25+
rr := httptest.NewRecorder()
26+
writeLivez(rr, func() bool { return false }, func(time.Duration) bool { return false }, time.Minute)
27+
assert.Equal(t, http.StatusServiceUnavailable, rr.Code)
28+
assert.Contains(t, rr.Body.String(), "not ready")
29+
})
30+
31+
t.Run("stale progress", func(t *testing.T) {
32+
t.Parallel()
33+
rr := httptest.NewRecorder()
34+
writeLivez(rr, func() bool { return true }, func(time.Duration) bool { return true }, time.Minute)
35+
assert.Equal(t, http.StatusServiceUnavailable, rr.Code)
36+
assert.Contains(t, rr.Body.String(), "stale")
37+
})
38+
39+
t.Run("ok", func(t *testing.T) {
40+
t.Parallel()
41+
rr := httptest.NewRecorder()
42+
writeLivez(rr, func() bool { return true }, func(time.Duration) bool { return false }, time.Minute)
43+
assert.Equal(t, http.StatusOK, rr.Code)
44+
assert.Equal(t, "ok\n", rr.Body.String())
45+
})
46+
}
47+
48+
func TestProgressTimeoutFromEnv(t *testing.T) {
49+
t.Parallel()
50+
assert.Equal(t, 10*time.Minute, progressTimeoutFromEnv(""))
51+
assert.Equal(t, time.Duration(0), progressTimeoutFromEnv("0"))
52+
assert.Equal(t, 30*time.Minute, progressTimeoutFromEnv("1800"))
53+
}
54+
1755
func TestProcessorSinkErrorMaxRetriesFromEnv(t *testing.T) {
1856
assert.Equal(t, 0, processorSinkErrorMaxRetriesFromEnv(""))
1957
assert.Equal(t, 0, processorSinkErrorMaxRetriesFromEnv("-1"))

internal/connectors/base.go

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -185,6 +185,21 @@ func (p *progressRecorder) notifyProgress() {
185185
}
186186
}
187187

188+
// AckMessages calls Ack on each message (commits source offsets when configured).
189+
func AckMessages(msgs []*types.Message) {
190+
for _, m := range msgs {
191+
if m.Ack != nil {
192+
m.Ack()
193+
}
194+
}
195+
}
196+
197+
// AckMessagesAndNotifyProgress commits offsets and updates liveness progress after a successful batch.
198+
func (p *progressRecorder) AckMessagesAndNotifyProgress(msgs []*types.Message) {
199+
AckMessages(msgs)
200+
p.notifyProgress()
201+
}
202+
188203
// SetConnectorInfo sets the connector type and role for metrics.
189204
func (c *connectorMetadata) SetConnectorInfo(connectorType, role string) {
190205
c.connectorType = connectorType
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
/*
2+
Copyright 2024.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package connectors
18+
19+
import (
20+
"sync/atomic"
21+
"testing"
22+
23+
"github.com/dataflow-operator/dataflow/internal/types"
24+
"github.com/stretchr/testify/assert"
25+
)
26+
27+
func TestAckMessagesAndNotifyProgress(t *testing.T) {
28+
t.Parallel()
29+
30+
var progressCalls atomic.Int32
31+
var ackCalls atomic.Int32
32+
33+
rec := &progressRecorder{}
34+
rec.SetProgressCallback(func() {
35+
progressCalls.Add(1)
36+
})
37+
38+
msg := types.NewMessage([]byte(`{}`))
39+
msg.Ack = func() { ackCalls.Add(1) }
40+
41+
rec.AckMessagesAndNotifyProgress([]*types.Message{msg})
42+
43+
assert.Equal(t, int32(1), ackCalls.Load())
44+
assert.Equal(t, int32(1), progressCalls.Load())
45+
}
46+
47+
func TestAckMessages_NoAckCallback(t *testing.T) {
48+
t.Parallel()
49+
50+
var progressCalls atomic.Int32
51+
rec := &progressRecorder{}
52+
rec.SetProgressCallback(func() {
53+
progressCalls.Add(1)
54+
})
55+
56+
rec.AckMessagesAndNotifyProgress([]*types.Message{types.NewMessage([]byte(`{}`))})
57+
assert.Equal(t, int32(1), progressCalls.Load())
58+
}

internal/connectors/batch_writer_test.go

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,37 @@ func TestRunBatchWriteLoop_InFlightCancel(t *testing.T) {
174174
assert.True(t, gotDetached.Load())
175175
}
176176

177+
func TestRunBatchWriteLoop_OnAck_NotifiesProgress(t *testing.T) {
178+
t.Parallel()
179+
180+
ctx := context.Background()
181+
ch := make(chan *types.Message, 1)
182+
ch <- types.NewMessage([]byte(`{}`))
183+
close(ch)
184+
185+
var progressCalls int
186+
sink := &progressRecorder{}
187+
sink.SetProgressCallback(func() {
188+
progressCalls++
189+
})
190+
191+
err := RunBatchWriteLoop(ctx, ch, BatchWriteConfig{MaxBatchSize: 1, FlushInterval: 0}, BatchWriteOptions{
192+
Logger: logr.Discard(),
193+
OnFlush: func(_ context.Context, _ []*types.Message) error {
194+
return nil
195+
},
196+
OnAck: func(msgs []*types.Message) {
197+
sink.AckMessagesAndNotifyProgress(msgs)
198+
},
199+
})
200+
if err != nil {
201+
t.Fatalf("RunBatchWriteLoop: %v", err)
202+
}
203+
if progressCalls != 1 {
204+
t.Fatalf("progressCalls = %d, want 1", progressCalls)
205+
}
206+
}
207+
177208
func TestRunBatchWriteLoop_OnAck(t *testing.T) {
178209
ctx := context.Background()
179210
ch := make(chan *types.Message, 1)

internal/connectors/clickhouse.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -254,6 +254,7 @@ type ClickHouseSinkConnector struct {
254254
baseConnector
255255
connectorLogger
256256
connectorMetadata
257+
progressRecorder
257258
rawModeConfig
258259
flattenMetadataSinkState
259260
config *v1.ClickHouseSinkSpec
@@ -734,6 +735,9 @@ func (c *ClickHouseSinkConnector) Write(ctx context.Context, messages <-chan *ty
734735
c.logger.V(1).Info("Received message for ClickHouse", logkeys.MessageID, types.MessageID(msg), "messageNumber", messageCount, "table", c.config.Table, "fields", getKeys(data))
735736
return true
736737
},
738+
OnAck: func(msgs []*types.Message) {
739+
c.AckMessagesAndNotifyProgress(msgs)
740+
},
737741
})
738742
}
739743

internal/connectors/kafka.go

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,10 +47,14 @@ import (
4747
)
4848

4949
// KafkaSourceConnector implements SourceConnector for Kafka
50+
// defaultKafkaSourceProgressHeartbeat is how often an active consumer group session updates liveness while idle.
51+
const defaultKafkaSourceProgressHeartbeat = 60 * time.Second
52+
5053
type KafkaSourceConnector struct {
5154
baseConnector
5255
connectorLogger
5356
connectorMetadata
57+
progressRecorder
5458
config *v1.KafkaSourceSpec
5559
consumer sarama.ConsumerGroup
5660
channelBufferSize int
@@ -65,6 +69,8 @@ type KafkaSourceConnector struct {
6569
testConsumeFunc func(ctx context.Context, topics []string, handler sarama.ConsumerGroupHandler) error
6670
// readErrCh receives fatal errors from the consumer after Read returns; set during Read.
6771
readErrCh chan error
72+
// progressHeartbeatInterval overrides defaultKafkaSourceProgressHeartbeat when > 0 (tests only).
73+
progressHeartbeatInterval time.Duration
6874
}
6975

7076
// schemaCache caches Avro schemas by ID
@@ -792,6 +798,13 @@ type kafkaConsumerGroupHandler struct {
792798
readyOnce sync.Once // Protects ready channel from being closed multiple times
793799
}
794800

801+
func (k *KafkaSourceConnector) kafkaProgressHeartbeatInterval() time.Duration {
802+
if k.progressHeartbeatInterval > 0 {
803+
return k.progressHeartbeatInterval
804+
}
805+
return defaultKafkaSourceProgressHeartbeat
806+
}
807+
795808
func (h *kafkaConsumerGroupHandler) Setup(sarama.ConsumerGroupSession) error {
796809
// Use sync.Once to ensure channel is closed only once
797810
// This protects against multiple Setup calls during rebalancing
@@ -811,6 +824,7 @@ func (h *kafkaConsumerGroupHandler) Setup(sarama.ConsumerGroupSession) error {
811824
}()
812825
close(h.ready)
813826
})
827+
h.connector.notifyProgress()
814828
return nil
815829
}
816830

@@ -823,6 +837,9 @@ func (h *kafkaConsumerGroupHandler) Cleanup(sarama.ConsumerGroupSession) error {
823837
const kafkaMarkChannelBuffer = 256
824838

825839
func (h *kafkaConsumerGroupHandler) ConsumeClaim(session sarama.ConsumerGroupSession, claim sarama.ConsumerGroupClaim) error {
840+
heartbeat := time.NewTicker(h.connector.kafkaProgressHeartbeatInterval())
841+
defer heartbeat.Stop()
842+
826843
markChan := make(chan *sarama.ConsumerMessage, kafkaMarkChannelBuffer)
827844

828845
markPending := func(message *sarama.ConsumerMessage) {
@@ -845,6 +862,8 @@ func (h *kafkaConsumerGroupHandler) ConsumeClaim(session sarama.ConsumerGroupSes
845862

846863
for {
847864
select {
865+
case <-heartbeat.C:
866+
h.connector.notifyProgress()
848867
case message := <-claim.Messages():
849868
if message == nil {
850869
drainMarks()
@@ -907,6 +926,7 @@ type KafkaSinkConnector struct {
907926
baseConnector
908927
connectorLogger
909928
connectorMetadata
929+
progressRecorder
910930
config *v1.KafkaSinkSpec
911931
producer sarama.SyncProducer
912932
}
@@ -1059,6 +1079,7 @@ func (k *KafkaSinkConnector) Write(ctx context.Context, messages <-chan *types.M
10591079
if msg.Ack != nil {
10601080
msg.Ack()
10611081
}
1082+
k.notifyProgress()
10621083
}
10631084
}
10641085
}

0 commit comments

Comments
 (0)