Some checks failed
Server CI / Postgres (shard 0) (push) Has been skipped
Server CI / Postgres (shard 3) (push) Has been skipped
BuildEnv Docker Image / build-image (push) Failing after 4s
BuildEnv Docker Image / build-image-fips (push) Failing after 4s
Migration Review & Release Notes / Generate migration review & release notes (push) Failing after 3s
Server CI / Build Environment (push) Failing after 3s
Server CI / Postgres (shard 1) (push) Has been skipped
Tools CI / check-style (mattermost-govet) (push) Failing after 4s
Server CI / Postgres (shard 2) (push) Has been skipped
Tools CI / check-style (mmgotool) (push) Failing after 3s
Server CI / Elasticsearch v8 Compatibility (push) Has been skipped
Server CI / OpenSearch v2 Compatibility (push) Has been skipped
Server CI / Postgres FIPS (shard 0) (push) Has been skipped
Tools CI / Test (mattermost-govet) (push) Failing after 3s
Server CI / Postgres FIPS (shard 1) (push) Has been skipped
Server CI / Postgres FIPS (shard 2) (push) Has been skipped
Tools CI / Test (mmgotool) (push) Failing after 3s
Server CI / Postgres FIPS (shard 3) (push) Has been skipped
Server CI / Run mmctl tests (push) Has been skipped
Server CI / Run mmctl tests (FIPS) (push) Has been skipped
Server CI / Merge Postgres Test Results (push) Has been skipped
Server CI / Merge Postgres FIPS Test Results (push) Has been skipped
Server CI / Check backport migrations (push) Has been skipped
Server CI / Check generated files (push) Has been skipped
Server CI / check-style (push) Has been skipped
Server CI / Vet API (push) Has been skipped
Server CI / Build mattermost server app (push) Has been skipped
Server CI / Server CI Complete (push) Failing after 2s
Server CI / Server CI Artifacts (push) Has been skipped
Server CI / Sentry Upload (push) Has been skipped
Server CI / Server CI Report (push) Failing after 5m1s
Server CI Nightly Race / Race Detector (push) Has been skipped
CodeQL / Analyze (go) (push) Has been cancelled
CodeQL / Analyze (javascript) (push) Has been cancelled
Webapp CI Cache Warm / Warm Webapp node_modules Cache (push) Has been cancelled
E2E CI Cache Warm / Warm E2E npm Registry Cache (push) Has been cancelled
API / build (push) Has been cancelled
Web App CI / check-types (push) Has been cancelled
Web App CI / check-lint (push) Has been cancelled
Web App CI / check-i18n (push) Has been cancelled
Web App CI / check-external-links (push) Has been cancelled
Web App CI / test (platform) (push) Has been cancelled
Web App CI / test (mattermost-redux) (push) Has been cancelled
Web App CI / test (channels shard 1/4) (push) Has been cancelled
Web App CI / test (channels shard 2/4) (push) Has been cancelled
Web App CI / test (channels shard 3/4) (push) Has been cancelled
Web App CI / test (channels shard 4/4) (push) Has been cancelled
Web App CI / upload-coverage (push) Has been cancelled
Web App CI / build (push) Has been cancelled
YAML Lint / yamllint (push) Has been cancelled
Push mirrored docker images / cd/Push mirrored docker images (push) Has been cancelled
docs-cd / Build and deploy docs (push) Has been cancelled
Docs CI / build (push) Has been cancelled
E2E Cache Warm: Cypress deps / Warm Cypress deps cache (push) Has been cancelled
Scorecards supply-chain security / Scorecard analysis (push) Has been cancelled
Server CI Weekly / Postgres FIPS (push) Has been skipped
Server CI Weekly / Run mmctl tests (FIPS) (push) Has been skipped
Server CI Weekly / Postgres with binary parameters (push) Has been skipped
Server CI Weekly / Build Environment (push) Failing after 1s
Server CI Cache Warm / Warm Go Module and Build Cache (push) Failing after 2s
Server CI Nightly Race / Build Environment (push) Failing after 1s
217 lines
7.3 KiB
Go
217 lines
7.3 KiB
Go
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
|
// See LICENSE.txt for license information.
|
|
|
|
package app
|
|
|
|
import (
|
|
"net/http"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/mattermost/mattermost/server/public/model"
|
|
"github.com/mattermost/mattermost/server/public/shared/mlog"
|
|
"github.com/mattermost/mattermost/server/public/shared/request"
|
|
"github.com/mattermost/mattermost/server/v8/channels/store"
|
|
)
|
|
|
|
// Backoff bounds for the guard-cache reload retry. Package vars (not consts) so tests can shrink
|
|
// them via t.Cleanup-restored override.
|
|
var (
|
|
guardCacheRetryInitialDelay = 1 * time.Second
|
|
guardCacheRetryMaxDelay = 5 * time.Minute
|
|
)
|
|
|
|
const clusterEventInvalidateChannelGuardCache = model.ClusterEvent("inv_channel_guards")
|
|
|
|
// reloadGuardCache scans the ChannelGuards table and atomically replaces the in-memory cache with
|
|
// the result. Used both at startup (from NewChannels) and from the cluster invalidation handler.
|
|
// Forces a master read because all callers (post-write reload, cluster invalidation) can race with
|
|
// replica lag.
|
|
func (ch *Channels) reloadGuardCache(rctx request.CTX, s store.Store) error {
|
|
guards, err := s.ChannelGuard().GetAll(store.RequestContextWithMaster(rctx))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
fresh := &sync.Map{}
|
|
grouped := map[string][]*store.ChannelGuard{}
|
|
for _, g := range guards {
|
|
grouped[g.ChannelId] = append(grouped[g.ChannelId], g)
|
|
}
|
|
for channelID, slice := range grouped {
|
|
fresh.Store(channelID, slice)
|
|
}
|
|
|
|
ch.guardCache.Store(fresh)
|
|
return nil
|
|
}
|
|
|
|
// getGuardsForChannel returns the cached guard slice for a channel, or nil if none.
|
|
func (ch *Channels) getGuardsForChannel(channelID string) []*store.ChannelGuard {
|
|
m := ch.guardCache.Load()
|
|
if m == nil {
|
|
return nil
|
|
}
|
|
v, ok := m.Load(channelID)
|
|
if !ok {
|
|
return nil
|
|
}
|
|
guards, _ := v.([]*store.ChannelGuard)
|
|
return guards
|
|
}
|
|
|
|
// clusterInvalidateGuardCacheHandler is registered as the receive-side handler for
|
|
// clusterEventInvalidateChannelGuardCache. The handler refetches the entire table.
|
|
func (ch *Channels) clusterInvalidateGuardCacheHandler(msg *model.ClusterMessage) {
|
|
rctx := request.EmptyContext(ch.srv.Log())
|
|
if err := ch.reloadGuardCache(rctx, ch.srv.Store()); err != nil {
|
|
ch.srv.Log().Warn(
|
|
"Failed to reload channel guard cache after cluster invalidation; retry scheduled",
|
|
mlog.String("event", string(msg.Event)),
|
|
mlog.Err(err),
|
|
)
|
|
ch.scheduleGuardCacheReloadRetry()
|
|
}
|
|
}
|
|
|
|
// broadcastChannelGuardInvalidation tells the rest of the cluster to refetch their guard caches.
|
|
// The payload is intentionally empty.
|
|
func (ch *Channels) broadcastChannelGuardInvalidation() {
|
|
cluster := ch.srv.platform.Cluster()
|
|
if cluster == nil {
|
|
return
|
|
}
|
|
|
|
msg := &model.ClusterMessage{
|
|
Event: clusterEventInvalidateChannelGuardCache,
|
|
SendType: model.ClusterSendReliable,
|
|
WaitForAllToSend: true,
|
|
}
|
|
cluster.SendClusterMessage(msg)
|
|
}
|
|
|
|
// RegisterChannelGuard records that pluginID claims channelID. The caller's pluginID is expected to
|
|
// be lowercased.
|
|
func (a *App) RegisterChannelGuard(rctx request.CTX, channelID, pluginID string) *model.AppError {
|
|
if channelID == "" {
|
|
return model.NewAppError("RegisterChannelGuard", "app.channel_guard.register.empty_channel.app_error", nil, "", http.StatusBadRequest)
|
|
}
|
|
if !model.IsValidId(channelID) {
|
|
return model.NewAppError("RegisterChannelGuard", "app.channel_guard.invalid_channel.app_error", nil, "", http.StatusBadRequest)
|
|
}
|
|
|
|
guard := &store.ChannelGuard{
|
|
ChannelId: channelID,
|
|
PluginId: pluginID,
|
|
CreatedAt: model.GetMillis(),
|
|
}
|
|
if err := a.Srv().Store().ChannelGuard().Save(rctx, guard); err != nil {
|
|
return model.NewAppError("RegisterChannelGuard", "app.channel_guard.register.app_error", nil, err.Error(), http.StatusInternalServerError).Wrap(err)
|
|
}
|
|
|
|
ch := a.Channels()
|
|
if err := ch.reloadGuardCache(rctx, a.Srv().Store()); err != nil {
|
|
a.Srv().Log().Warn(
|
|
"Failed to reload channel guard cache after Register; retry scheduled",
|
|
mlog.String("channel_id", channelID),
|
|
mlog.String("plugin_id", pluginID),
|
|
mlog.Err(err),
|
|
)
|
|
ch.scheduleGuardCacheReloadRetry()
|
|
}
|
|
ch.broadcastChannelGuardInvalidation()
|
|
return nil
|
|
}
|
|
|
|
// UnregisterChannelGuard removes pluginID's claim on channelID. If pluginID has no claim on the
|
|
// channel, this is a no-op (returns nil). The store-level DELETE matches by both ChannelId and
|
|
// PluginId, so other plugins' claims on the same channel are left untouched.
|
|
func (a *App) UnregisterChannelGuard(rctx request.CTX, channelID, pluginID string) *model.AppError {
|
|
if channelID == "" {
|
|
return model.NewAppError("UnregisterChannelGuard", "app.channel_guard.unregister.empty_channel.app_error", nil, "", http.StatusBadRequest)
|
|
}
|
|
if !model.IsValidId(channelID) {
|
|
return model.NewAppError("UnregisterChannelGuard", "app.channel_guard.invalid_channel.app_error", nil, "", http.StatusBadRequest)
|
|
}
|
|
|
|
rowsAffected, err := a.Srv().Store().ChannelGuard().Delete(rctx, channelID, pluginID)
|
|
if err != nil {
|
|
return model.NewAppError("UnregisterChannelGuard", "app.channel_guard.unregister.app_error", nil, err.Error(), http.StatusInternalServerError).Wrap(err)
|
|
}
|
|
if rowsAffected == 0 {
|
|
a.Srv().Log().Warn(
|
|
"UnregisterChannelGuard removed no rows; pluginID does not match any guard for this channel",
|
|
mlog.String("error_id", "unregister_no_matching_guard"),
|
|
mlog.String("channel_id", channelID),
|
|
mlog.String("plugin_id", pluginID),
|
|
)
|
|
}
|
|
|
|
ch := a.Channels()
|
|
if err := ch.reloadGuardCache(rctx, a.Srv().Store()); err != nil {
|
|
a.Srv().Log().Warn(
|
|
"Failed to reload channel guard cache after Unregister; retry scheduled",
|
|
mlog.String("channel_id", channelID),
|
|
mlog.String("plugin_id", pluginID),
|
|
mlog.Err(err),
|
|
)
|
|
ch.scheduleGuardCacheReloadRetry()
|
|
}
|
|
ch.broadcastChannelGuardInvalidation()
|
|
return nil
|
|
}
|
|
|
|
// scheduleGuardCacheReloadRetry kicks off a single in-flight retry goroutine that calls
|
|
// reloadGuardCache with exponential backoff until success or until the server is shutting down.
|
|
// Multiple concurrent calls collapse to a single retry — useful when Register, Unregister, the
|
|
// cluster handler, and the startup loader can all see the same DB outage simultaneously.
|
|
//
|
|
// Returns true if a new retry goroutine was scheduled, false if one was already in flight. Call
|
|
// sites can ignore the return value; tests use it to assert single-flight semantics.
|
|
func (ch *Channels) scheduleGuardCacheReloadRetry() bool {
|
|
if !ch.guardCacheRetryInFlight.CompareAndSwap(false, true) {
|
|
return false
|
|
}
|
|
go ch.runGuardCacheReloadRetry()
|
|
return true
|
|
}
|
|
|
|
func (ch *Channels) runGuardCacheReloadRetry() {
|
|
defer ch.guardCacheRetryInFlight.Store(false)
|
|
rctx := request.EmptyContext(ch.srv.Log())
|
|
|
|
delay := guardCacheRetryInitialDelay
|
|
for attempt := 1; ; attempt++ {
|
|
timer := time.NewTimer(delay)
|
|
select {
|
|
case <-ch.interruptQuitChan:
|
|
timer.Stop()
|
|
ch.srv.Log().Info(
|
|
"Channel guard cache reload retry cancelled by shutdown",
|
|
mlog.Int("attempt", attempt),
|
|
)
|
|
return
|
|
case <-timer.C:
|
|
}
|
|
|
|
if err := ch.reloadGuardCache(rctx, ch.srv.Store()); err != nil {
|
|
ch.srv.Log().Info(
|
|
"Channel guard cache reload retry attempt failed; will retry",
|
|
mlog.Int("attempt", attempt),
|
|
mlog.Err(err),
|
|
)
|
|
delay *= 2
|
|
if delay > guardCacheRetryMaxDelay {
|
|
delay = guardCacheRetryMaxDelay
|
|
}
|
|
continue
|
|
}
|
|
|
|
ch.srv.Log().Info(
|
|
"Channel guard cache reload retry succeeded",
|
|
mlog.Int("attempt", attempt),
|
|
)
|
|
return
|
|
}
|
|
}
|