-
Notifications
You must be signed in to change notification settings - Fork 145
MM-70280 Applying dedup on channel subscription posts #1339
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from 4 commits
fcc2d07
86dfd6a
98f0fa6
5221ff7
1b5583a
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,116 @@ | ||
| // Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved. | ||
| // See LICENSE.txt for license information. | ||
|
|
||
| package main | ||
|
|
||
| import ( | ||
| "sync" | ||
| "sync/atomic" | ||
| "testing" | ||
| "time" | ||
|
|
||
| jira "github.com/andygrunwald/go-jira" | ||
| "github.com/mattermost/mattermost/server/public/model" | ||
| "github.com/mattermost/mattermost/server/public/plugin/plugintest" | ||
| "github.com/mattermost/mattermost/server/public/plugin/plugintest/mock" | ||
| "github.com/mattermost/mattermost/server/public/pluginapi" | ||
| "github.com/stretchr/testify/require" | ||
| ) | ||
|
|
||
| func newTestChannelWebhook() *webhook { | ||
| return &webhook{ | ||
| JiraWebhook: &JiraWebhook{ | ||
| Issue: jira.Issue{ID: "10001", Key: "PROJ-1"}, | ||
| }, | ||
| headline: "Actor **commented** on PROJ-1", | ||
| } | ||
| } | ||
|
|
||
| // isDedupClaimOptions matches the KV options PostToChannel's dedup claim must | ||
| // use: an atomic write (no old value expected, since the key shouldn't yet | ||
| // exist) with the shared webhook dedup TTL. | ||
| func isDedupClaimOptions(opts model.PluginKVSetOptions) bool { | ||
| return opts.Atomic && opts.OldValue == nil && opts.ExpireInSeconds == int64(webhookDedupTTL/time.Second) | ||
| } | ||
|
|
||
| func TestPostToChannelDeduplicatesConcurrentDeliveries(t *testing.T) { | ||
| t.Run("concurrent deliveries for the same channel post only once", func(t *testing.T) { | ||
| api := &plugintest.API{} | ||
|
|
||
| var kvWinners atomic.Int32 | ||
| api.On("KVSetWithOptions", mock.AnythingOfType("string"), mock.Anything, mock.MatchedBy(isDedupClaimOptions)).Return(func(string, []byte, model.PluginKVSetOptions) (bool, *model.AppError) { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This stub matches the key with A map based fake makes them real tho var mu sync.Mutex
claimed := map[string]bool{}
api.On("KVSetWithOptions", mock.AnythingOfType("string"), mock.Anything, mock.MatchedBy(isDedupClaimOptions)).
Return(func(key string, _ []byte, _ model.PluginKVSetOptions) (bool, *model.AppError) {
mu.Lock()
defer mu.Unlock()
if claimed[key] {
return false, nil
}
claimed[key] = true
return true, nil
}) |
||
| if kvWinners.Add(1) == 1 { | ||
| return true, nil | ||
| } | ||
| return false, nil | ||
| }) | ||
| api.On("CreatePost", mock.AnythingOfType("*model.Post")).Return(&model.Post{Id: "post1"}, nil).Once() | ||
| api.On("LogDebug", mockAnythingOfTypeBatch("string", 3)...).Return() | ||
|
|
||
| p := &Plugin{} | ||
| p.SetAPI(api) | ||
| p.client = pluginapi.NewClient(api, p.Driver) | ||
|
|
||
| const callers = 25 | ||
| var wg sync.WaitGroup | ||
| wg.Add(callers) | ||
| start := make(chan struct{}) | ||
| for i := 0; i < callers; i++ { | ||
| go func() { | ||
| defer wg.Done() | ||
| <-start | ||
| _, _, err := newTestChannelWebhook().PostToChannel(p, "instance-1", "channel-1", "bot-user-id", "") | ||
| require.NoError(t, err) | ||
| }() | ||
| } | ||
| close(start) | ||
| wg.Wait() | ||
|
|
||
| api.AssertExpectations(t) | ||
| }) | ||
|
|
||
| t.Run("overlapping subscriptions on the same channel still post only once", func(t *testing.T) { | ||
| // Two subscriptions on the same channel produce identical webhook content | ||
| // but different subscription names; the dedup key must ignore the name. | ||
| api := &plugintest.API{} | ||
| var kvWinners atomic.Int32 | ||
| api.On("KVSetWithOptions", mock.AnythingOfType("string"), mock.Anything, mock.MatchedBy(isDedupClaimOptions)).Return(func(string, []byte, model.PluginKVSetOptions) (bool, *model.AppError) { | ||
| if kvWinners.Add(1) == 1 { | ||
| return true, nil | ||
| } | ||
| return false, nil | ||
| }).Twice() | ||
| api.On("CreatePost", mock.AnythingOfType("*model.Post")).Return(&model.Post{Id: "post1"}, nil).Once() | ||
| api.On("LogDebug", mockAnythingOfTypeBatch("string", 3)...).Return().Once() | ||
|
|
||
| p := &Plugin{} | ||
| p.SetAPI(api) | ||
| p.client = pluginapi.NewClient(api, p.Driver) | ||
|
|
||
| _, _, err := newTestChannelWebhook().PostToChannel(p, "instance-1", "channel-1", "bot-user-id", "subscription-a") | ||
| require.NoError(t, err) | ||
|
|
||
| post, _, err := newTestChannelWebhook().PostToChannel(p, "instance-1", "channel-1", "bot-user-id", "subscription-b") | ||
| require.NoError(t, err) | ||
| require.Nil(t, post) | ||
|
|
||
| api.AssertExpectations(t) | ||
| }) | ||
|
|
||
| t.Run("different channels each get their own post", func(t *testing.T) { | ||
| api := &plugintest.API{} | ||
| api.On("KVSetWithOptions", mock.AnythingOfType("string"), mock.Anything, mock.MatchedBy(isDedupClaimOptions)).Return(true, (*model.AppError)(nil)).Twice() | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Same issue as the stub in the first subtest: this always returns |
||
| api.On("CreatePost", mock.AnythingOfType("*model.Post")).Return(&model.Post{Id: "post1"}, nil).Twice() | ||
|
|
||
| p := &Plugin{} | ||
| p.SetAPI(api) | ||
| p.client = pluginapi.NewClient(api, p.Driver) | ||
|
|
||
| _, _, err := newTestChannelWebhook().PostToChannel(p, "instance-1", "channel-1", "bot-user-id", "") | ||
| require.NoError(t, err) | ||
| _, _, err = newTestChannelWebhook().PostToChannel(p, "instance-1", "channel-2", "bot-user-id", "") | ||
| require.NoError(t, err) | ||
|
|
||
| api.AssertExpectations(t) | ||
| }) | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The dedup key is claimed here but never released if CreatePost fails a few lines below. If the first of several duplicate deliveries claims the key and then fails to post some deliveries are skipped and the event is lost.
Suggest releasing the claim on the failure path when
claimedis true:PostNotifications has the same gap so a shared
releaseDedupKeyhelper may be cleaner than duplicating this.