mirror of
https://github.com/hanzoai/team.git
synced 2026-09-30 01:57:35 +00:00
The server module named github.com/hanzoai/team/server/v8 — a path that resolves to the retired workspace's repository, not this one — while server/public still published github.com/mattermost/mattermost/server/public. Both are github.com/hanzoteam/server/... now, which is where this code actually lives. That second half is what made the binary unbootable. Upstream's public path stayed claimed by upstream, mattermost-plugin-ai imported it, and the build linked public/model twice; public/plugin's gob.Register then ran twice under one name and panicked at init, before main. Reverting the rename traded the brand for a working binary. Rebranding the plugin alongside it needs no such trade: the agents fork is github.com/hanzoteam/agents/v2 against our public module, so no module named mattermost/mattermost is in the graph at all. Measured: `go list -deps ./cmd/mattermost` names exactly one public/model, and it is ours.
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/hanzoteam/server/server/public/model"
|
|
"github.com/hanzoteam/server/server/public/shared/mlog"
|
|
"github.com/hanzoteam/server/server/public/shared/request"
|
|
"github.com/hanzoteam/server/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
|
|
}
|
|
}
|