Files
24045f5b7c MM-60441: Re-index public channels when a user joins a team (#33400)
* Index all public channels when a user joins a team

* Precompute team members for indexChannelsForTeam

* Refactor RequestContextWithMaster to store package

This way, we can import it from both the sqlstore and the searchlayer
packages. The alternative for this is duplicating the code in those two
packages, but that will *not* work:

The context package expects custom types for the keys stored in it, so
that different packages never clash with each other when trying to
register a new key. See the docs for the WithValue function:
https://pkg.go.dev/context#WithValue

If we try to duplicate the storeContextKey type in both the sqlstore and
searchlayer packages, although they *look* the same, they are not, and
HasMaster will fail to get the value of the storeContextKey(useMaster)
key if it's from the other package.

* Use master in call to GetTeamMembersForChannel

In GetTeamMembersForChannel, use the DB from the newly passed context,
which will be the receiving context everywhere except in the call done
from indexChannelsForTeam, to avoid the read after write issue when
saving a team member.

* Fix GetPublicChannelsForTeam paging

We were using the page and perPage arguments as is in the call to
GetPublicChannelsForTeam, but that function expects and offset and a
limit as understood by SQL. Although perPage and limit are
interchangeable, offset is not equal to page, but to page * perPage.

* Add a synchronous bulk indexer for Opensearch

* Implement Opensearch's SyncBulkIndexChannels

* Add a synchronous bulk indexer for Elasticsearch

* Implement Elasticsearch's SynkBulkIndexChannels

* Test SyncBulkIndexChannels

* make mocks

* Bulk index channels on indexChannelsForTeam

* Handle error from SyncBulkIndexChannels

* Fix style

* Revert indexChannelWithTeamMembers refactor

* Remove defensive code on sync bulk processor

* Revert "Add a synchronous bulk indexer for Opensearch"

This reverts commit bfe4671d96bffa9ca27ed3c655fc5527b72bbafb.

* Revert "Add a synchronous bulk indexer for Elasticsearch"

This reverts commit 6643ae3f30c461544d0861aec7dee1f24e507c37.

* Refactor bulk indexers with a common interface

* Test all the different implementations

Assisted by Claude

* Remove debug statements

* Refactor common code into _stop

* Rename getUserIDsFor{,Private}Channel

* Wrap error

* Make perPage a const

* Fix typos

* Call GetTeamsForUser only if needed

* Differentiate errors for sync/async processors

---------

Co-authored-by: Ibrahim Serdar Acikgoz <[email protected]>
Co-authored-by: Mattermost Build <[email protected]>
2025-08-25 19:28:19 +02:00

185 lines
4.9 KiB
Go

// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.enterprise for license information.
package elasticsearch
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"sync"
"time"
elastic "github.com/elastic/go-elasticsearch/v8"
"github.com/elastic/go-elasticsearch/v8/esutil"
esTypes "github.com/elastic/go-elasticsearch/v8/typedapi/types"
"github.com/mattermost/mattermost/server/public/shared/mlog"
"github.com/mattermost/mattermost/server/v8/enterprise/elasticsearch/common"
)
// DataBulkClient is an Elasticsearch bulk client based on the
// go-elasticsearch/v8/esutil.BulkIndexer type.
// It supports time- and size-based thresholds, but not a threshold on number
// of requests.
type DataBulkClient struct {
mut sync.Mutex
indexer esutil.BulkIndexer
client *elastic.TypedClient
bulkSettings common.BulkSettings
reqTimeout time.Duration
logger mlog.LoggerIFace
}
func NewDataBulkClient(bulkSettings common.BulkSettings,
client *elastic.TypedClient,
reqTimeout time.Duration,
logger mlog.LoggerIFace,
) (*DataBulkClient, error) {
if bulkSettings.FlushNumReqs > 0 {
return nil, fmt.Errorf("DataBulkClient does not support a threshold on number of requests")
}
indexer, err := newIndexer(client, bulkSettings, logger)
if err != nil {
return nil, err
}
return &DataBulkClient{
indexer: indexer,
client: client,
bulkSettings: bulkSettings,
reqTimeout: reqTimeout,
logger: logger,
}, nil
}
func newIndexer(client *elastic.TypedClient, bulkSettings common.BulkSettings, logger mlog.LoggerIFace) (esutil.BulkIndexer, error) {
// A zeroed FlushInterval means that there should be no time-based flush,
// but esutil.BulkIndexer defaults to 30 seconds if the interval is zero,
// so we pick a large enough interval
interval := bulkSettings.FlushInterval
if interval == 0 {
interval = 1 * time.Hour
}
return esutil.NewBulkIndexer(esutil.BulkIndexerConfig{
FlushBytes: bulkSettings.FlushBytes,
FlushInterval: interval,
Client: client,
OnError: func(ctx context.Context, err error) {
logger.Error("indexer error", mlog.Err(err))
},
OnFlushStart: func(ctx context.Context) context.Context {
logger.Debug("elasticsearch bulk indexer flush started")
return ctx
},
OnFlushEnd: func(context.Context) {
logger.Debug("elasticsearch bulk indexer flush ended")
},
})
}
func (b *DataBulkClient) onSuccess(_ context.Context, item esutil.BulkIndexerItem, _ esutil.BulkIndexerResponseItem) {
b.logger.Info("successfully added new bulk operation",
mlog.String("action", item.Action),
mlog.String("index", item.Index),
mlog.String("document_id", item.DocumentID),
)
}
func (b *DataBulkClient) onFailure(_ context.Context, item esutil.BulkIndexerItem, _ esutil.BulkIndexerResponseItem, err error) {
b.logger.Info("failed to add new bulk operation",
mlog.String("action", item.Action),
mlog.String("index", item.Index),
mlog.String("document_id", item.DocumentID),
mlog.Err(err),
)
}
func (b *DataBulkClient) IndexOp(op esTypes.IndexOperation, doc any) error {
b.mut.Lock()
defer b.mut.Unlock()
var bodyReader io.ReadSeeker
switch v := doc.(type) {
case []byte:
bodyReader = bytes.NewReader(v)
case json.RawMessage:
bodyReader = bytes.NewReader(v)
default:
body, err := json.Marshal(doc)
if err != nil {
return err
}
bodyReader = bytes.NewReader(body)
}
ctx, cancel := context.WithTimeout(context.Background(), b.reqTimeout)
defer cancel()
return b.indexer.Add(ctx, esutil.BulkIndexerItem{
Index: *op.Index_,
Action: "index",
DocumentID: *op.Id_,
Body: bodyReader,
OnSuccess: b.onSuccess,
OnFailure: b.onFailure,
})
}
func (b *DataBulkClient) DeleteOp(op esTypes.DeleteOperation) error {
b.mut.Lock()
defer b.mut.Unlock()
ctx, cancel := context.WithTimeout(context.Background(), b.reqTimeout)
defer cancel()
return b.indexer.Add(ctx, esutil.BulkIndexerItem{
Index: *op.Index_,
Action: "delete",
DocumentID: *op.Id_,
Body: nil,
OnSuccess: b.onSuccess,
OnFailure: b.onFailure,
})
}
func (b *DataBulkClient) _stop() error {
ctx, cancel := context.WithTimeout(context.Background(), b.reqTimeout)
defer cancel()
return b.indexer.Close(ctx)
}
func (b *DataBulkClient) Flush() error {
b.mut.Lock()
defer b.mut.Unlock()
// The esutil.BulkIndexer cannot be manually flushed, but it can be closed,
// which does flush all the contents.
if err := b._stop(); err != nil {
return fmt.Errorf("failed to close the BulkIndexer: %w", err)
}
// But calling Close essentially kills all the running processes, so we have
// to create a new one in order to restart it
indexer, err := newIndexer(b.client, b.bulkSettings, b.logger)
if err != nil {
return fmt.Errorf("failed to restart the BulkIndexer: %w", err)
}
b.indexer = indexer
return nil
}
func (b *DataBulkClient) Stop() error {
b.mut.Lock()
defer b.mut.Unlock()
b.logger.Info("Stopping Bulk processor")
return b._stop()
}