-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathdead_letter.go
More file actions
138 lines (131 loc) · 4.94 KB
/
Copy pathdead_letter.go
File metadata and controls
138 lines (131 loc) · 4.94 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
package events
import (
"context"
"sync"
"github.com/lestrrat-go/backoff/v2"
"github.com/memsql/errors"
"github.com/segmentio/kafka-go"
"github.com/singlestore-labs/events/eventmodels"
)
const (
deadLetterGroupPostfix = "-dead-letter"
deadLetterTopicPostfix = ".dead-letter"
)
// DeadLetterTopic returns the topic name to use for dead letters. The dead letter
// topics include the consumer group name because otherwise messages could be
// cross-delivered between consumer groups. It consumes and returns un-prefixed topics.
func DeadLetterTopic(unprefixedTopic string, consumerGroup ConsumerGroupName) string {
return unprefixedTopic + "." + consumerGroup.String() + deadLetterTopicPostfix
}
// startDeadLetterConsumers checks the handlers to see if any of them have onFailure set to
// use dead letter handling and if so creates the dead letter topics and starts dead letter
// consumers.
func (lib *Library[ID, TX, DB]) startDeadLetterConsumers(startupCtx context.Context, baseCtx context.Context, consumerGroup consumerGroupName, originalGroup *group, limiter *limit, allStarted *sync.WaitGroup, groupDone *sync.WaitGroup) {
preCreate := make([]string, 0, len(originalGroup.topics))
for topic, topicHandler := range originalGroup.topics {
var doCreate bool
for _, handler := range topicHandler.handlers {
if handler.isDeadLetter {
continue
}
switch handler.onFailure {
case eventmodels.OnFailureRetryLater, eventmodels.OnFailureSave:
doCreate = true
}
}
if !doCreate {
continue
}
dlTopic := DeadLetterTopic(topic, consumerGroup)
preCreate = append(preCreate, dlTopic)
// pre-configure the dead-letter topic to match the original topic
if config, ok := lib.getTopicConfig(topic); ok {
config.Topic = dlTopic
lib.SetTopicConfig(config)
}
}
if len(preCreate) == 0 {
return
}
// This shouldn't error because the precreate for the non-dead letter versions
// succeeded before this was called
err := lib.precreateTopicsForConsuming(startupCtx, consumerGroup, preCreate)
if err != nil {
if e := startupCtx.Err(); e == nil {
lib.logf(startupCtx, "[events] UNEXPECTED ERROR creating topics for dead letter consumption, not consuming dead letter topics: %+v", err)
}
return
}
var startConsumer bool
dlGroup := &group{
topics: make(map[string]*topicHandlers),
maxIdle: originalGroup.maxIdle,
}
for topic, topicHandler := range originalGroup.topics {
// iterate in the same order as the original handlers were registered
var setConfig bool
for _, handlerName := range topicHandler.handlerNames {
handler := topicHandler.handlers[handlerName]
if handler.isDeadLetter {
continue
}
if handler.onFailure != eventmodels.OnFailureRetryLater {
continue
}
startConsumer = true
dlTopic := DeadLetterTopic(topic, consumerGroup)
dlTopicHandler, ok := dlGroup.topics[dlTopic]
if !ok {
dlTopicHandler = &topicHandlers{
handlers: make(map[string]*registeredHandler),
}
dlGroup.topics[dlTopic] = dlTopicHandler
}
dlTopicHandler.addHandler(handlerName, eventmodels.OnFailureBlock, &lib.LibraryNoDB, handler.handler, []HandlerOpt{WithRetrying(true), IsDeadLetterHandler(true), WithQueueDepthLimit(maximumDeadLetterOutstanding)})
setConfig = true
}
if setConfig {
topicConfig, ok := lib.getTopicConfig(topic)
if ok {
lib.SetTopicConfig(topicConfig)
} else if lib.mustRegisterTopics {
panic(errors.Alertf("unexpected missing topic config for topic (%s)", topic))
}
topicConfig.Topic = DeadLetterTopic(topic, consumerGroup)
}
}
if startConsumer {
if debugConsumeStartup {
lib.logf(startupCtx, "[events] Debug: consume startwait +1 for %s", consumerGroup+deadLetterGroupPostfix)
}
allStarted.Add(1)
if debugShutdown {
lib.logf(startupCtx, "[events] Debug shutdown: allDone/groupDone +1 for %s", consumerGroup+deadLetterGroupPostfix)
}
groupDone.Add(1)
go lib.startConsumingGroup(startupCtx, baseCtx, consumerGroup+deadLetterGroupPostfix, dlGroup, limiter, false, allStarted, groupDone, true, nil, nil, nil)
}
}
func (lib *Library[ID, TX, DB]) produceToDeadLetter(ctx context.Context, consumerGroup consumerGroupName, handlerName string, msg kafka.Message) {
originalTopic := msg.Topic
msg.Topic = DeadLetterTopic(msg.Topic, consumerGroup)
b := backoffPolicy.Start(ctx)
var failures int
for {
err := lib.writer.WriteMessages(ctx, msg)
if err == nil {
if failures > 0 {
lib.logf(ctx, "[events] finally produced dead letter message (%s/%s) to Kafka after %d failure(s)", msg.Topic, string(msg.Key), failures)
} else {
lib.logf(ctx, "[events] produced dead letter message (%s/%s) to Kafka", msg.Topic, string(msg.Key))
}
DeadLetterProduceCounts.WithLabelValues(handlerName, originalTopic).Inc()
return
}
failures++
_ = lib.RecordErrorNoWait(ctx, "produceEvents", errors.Errorf("cannot produce dead letter message (%s/%s, %d failures) to Kafka: %w", msg.Topic, string(msg.Key), failures, err))
if !backoff.Continue(b) {
return
}
}
}