-
Notifications
You must be signed in to change notification settings - Fork 31
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: ignored duplicate Kafka messages (#373)
- Loading branch information
Showing
8 changed files
with
267 additions
and
62 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -75,11 +75,13 @@ pprof: | |
|
||
pre-commit: codegen proto test install runner testapi lint start | ||
|
||
codegen: generate manifests examples tests | ||
codegen: generate manifests examples tests $(GOBIN)/mockery | ||
go generate ./... | ||
|
||
$(GOBIN)/goreman: | ||
go install github.com/mattn/[email protected] | ||
$(GOBIN)/mockery: | ||
go install github.com/vektra/mockery/[email protected] | ||
|
||
# Run against the configured Kubernetes cluster in ~/.kube/config | ||
start: deploy build runner $(GOBIN)/goreman wait | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,118 @@ | ||
package monitor | ||
|
||
import ( | ||
"context" | ||
"fmt" | ||
"strconv" | ||
"sync" | ||
"time" | ||
|
||
sharedutil "github.com/argoproj-labs/argo-dataflow/shared/util" | ||
"github.com/go-redis/redis/v8" | ||
"github.com/prometheus/client_golang/prometheus" | ||
"github.com/prometheus/client_golang/prometheus/promauto" | ||
"k8s.io/apimachinery/pkg/util/wait" | ||
) | ||
|
||
var ( | ||
logger = sharedutil.NewLogger() | ||
duplicateCounter = promauto.NewCounterVec( | ||
prometheus.CounterOpts{ | ||
Subsystem: "sources", | ||
Name: "duplicate", | ||
Help: "Total number of duplicate messages, see https://github.com/argoproj-labs/argo-dataflow/blob/main/docs/METRICS.md#sources_duplicate", | ||
}, | ||
[]string{"sourceName"}, | ||
) | ||
missingCounter = promauto.NewCounterVec( | ||
prometheus.CounterOpts{ | ||
Subsystem: "sources", | ||
Name: "missing", | ||
Help: "Total number of missing messages, see https://github.com/argoproj-labs/argo-dataflow/blob/main/docs/METRICS.md#sources_missing", | ||
}, | ||
[]string{"sourceName"}, | ||
) | ||
) | ||
|
||
//go:generate mockery --exported --name=storage | ||
|
||
type storage interface { | ||
Get(ctx context.Context, key string) (string, error) | ||
Set(ctx context.Context, key string, value interface{}, expiration time.Duration) error | ||
} | ||
|
||
type redisStorage struct { | ||
rdb redis.Cmdable | ||
} | ||
|
||
func (r *redisStorage) Get(ctx context.Context, key string) (string, error) { | ||
return r.rdb.Get(ctx, key).Result() | ||
} | ||
|
||
func (r *redisStorage) Set(ctx context.Context, key string, value interface{}, expiration time.Duration) error { | ||
return r.rdb.Set(ctx, key, value, expiration).Err() | ||
} | ||
|
||
type impl struct { | ||
mu sync.Mutex | ||
db map[string]int64 | ||
pipelineName string | ||
stepName string | ||
storage storage | ||
} | ||
|
||
func (i *impl) Accept(ctx context.Context, sourceName, sourceURN string, partition int32, offset int64) (bool, error) { | ||
i.mu.Lock() | ||
defer i.mu.Unlock() | ||
key := fmt.Sprintf("%s/%s/%s/%d/offset", i.pipelineName, i.stepName, sourceURN, partition) | ||
if _, ok := i.db[key]; !ok { | ||
text, _ := i.storage.Get(ctx, key) | ||
if text == "" { // assume that this is the first time, and we are continuous | ||
i.db[key] = offset - 1 | ||
} else { | ||
lastOffset, err := strconv.ParseInt(text, 10, 64) | ||
if err != nil { | ||
return false, err | ||
} | ||
i.db[key] = lastOffset | ||
} | ||
} | ||
lastOffset := i.db[key] | ||
expectedOffset := lastOffset + 1 | ||
if offset < expectedOffset { | ||
duplicateCounter.WithLabelValues(sourceName).Inc() | ||
return false, nil | ||
} | ||
if offset > expectedOffset { | ||
missingCounter.WithLabelValues(sourceName).Inc() | ||
} else { | ||
i.db[key] = offset | ||
} | ||
return true, nil | ||
} | ||
|
||
func (i *impl) commitOffsets(ctx context.Context) { | ||
i.mu.Lock() | ||
defer i.mu.Unlock() | ||
for key, offset := range i.db { | ||
if err := i.storage.Set(ctx, key, offset, time.Hour*24*30); err != nil { | ||
logger.Error(err, "failed to set bit", "key", key, "offset", offset) | ||
} | ||
} | ||
} | ||
|
||
func New(ctx context.Context, pipelineName, stepName string) Interface { | ||
i := &impl{ | ||
sync.Mutex{}, | ||
map[string]int64{}, | ||
pipelineName, | ||
stepName, | ||
&redisStorage{redis.NewClient(&redis.Options{ | ||
Addr: "redis:6379", | ||
})}, | ||
} | ||
|
||
go wait.JitterUntilWithContext(ctx, i.commitOffsets, 3*time.Second, 1.2, true) | ||
|
||
return i | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,77 @@ | ||
package monitor | ||
|
||
import ( | ||
"context" | ||
"errors" | ||
"sync" | ||
"testing" | ||
"time" | ||
|
||
"github.com/argoproj-labs/argo-dataflow/runner/sidecar/monitor/mocks" | ||
"github.com/prometheus/client_golang/prometheus" | ||
io_prometheus_client "github.com/prometheus/client_model/go" | ||
"github.com/stretchr/testify/assert" | ||
) | ||
|
||
func Test_impl_Accept(t *testing.T) { | ||
ctx := context.Background() | ||
rdb := &mocks.Storage{} | ||
rdb.On("Get", ctx, "my-pl/my-step/my-urn/1/offset").Return("", errors.New("")) | ||
rdb.On("Get", ctx, "my-pl/my-step/my-urn/2/offset").Return("1", nil) | ||
i := &impl{ | ||
mu: sync.Mutex{}, | ||
db: map[string]int64{}, | ||
pipelineName: "my-pl", | ||
stepName: "my-step", | ||
storage: rdb, | ||
} | ||
t.Run("EmptyStorage", func(t *testing.T) { | ||
accept, err := i.Accept(ctx, "my-source", "my-urn", 1, 1) | ||
assert.NoError(t, err) | ||
assert.True(t, accept) | ||
assert.Equal(t, 0, duplicate(t)) | ||
assert.Equal(t, 0, missing(t)) | ||
}) | ||
t.Run("ExistingStorage", func(t *testing.T) { | ||
accept, err := i.Accept(ctx, "my-source", "my-urn", 2, 2) | ||
assert.NoError(t, err) | ||
assert.True(t, accept) | ||
assert.Equal(t, 0, duplicate(t)) | ||
assert.Equal(t, 0, missing(t)) | ||
}) | ||
t.Run("RepeatedOffset", func(t *testing.T) { | ||
accept, err := i.Accept(ctx, "my-source", "my-urn", 2, 2) | ||
assert.NoError(t, err) | ||
assert.False(t, accept) | ||
assert.Equal(t, 1, duplicate(t)) | ||
assert.Equal(t, 0, missing(t)) | ||
}) | ||
t.Run("SkippedOffset", func(t *testing.T) { | ||
accept, err := i.Accept(ctx, "my-source", "my-urn", 2, 4) | ||
assert.NoError(t, err) | ||
assert.True(t, accept) | ||
assert.Equal(t, 1, duplicate(t)) | ||
assert.Equal(t, 1, missing(t)) | ||
}) | ||
thirtyDays := time.Hour * 24 * 30 | ||
rdb.On("Set", ctx, "my-pl/my-step/my-urn/1/offset", int64(1), thirtyDays).Return(nil) | ||
rdb.On("Set", ctx, "my-pl/my-step/my-urn/2/offset", int64(2), thirtyDays).Return(nil) | ||
t.Run("CommitOffsets", func(t *testing.T) { | ||
i.commitOffsets(ctx) | ||
}) | ||
} | ||
|
||
func duplicate(t *testing.T) int { | ||
return counter(t, duplicateCounter) | ||
} | ||
|
||
func missing(t *testing.T) int { | ||
return counter(t, missingCounter) | ||
} | ||
|
||
func counter(t *testing.T, counter *prometheus.CounterVec) int { | ||
dto := &io_prometheus_client.Metric{} | ||
err := counter.WithLabelValues("my-source").Write(dto) | ||
assert.NoError(t, err) | ||
return int(*dto.Counter.Value) | ||
} |
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.