Move Elasticsearch to source available 🎉 🎉 (#29015)
* Move Elasticsearch to source available
Этот коммит содержится в:
коммит произвёл
GitHub
родитель
311381940d
Коммит
65ed87bda0
331
server/enterprise/elasticsearch/opensearch/aggregation_job.go
Обычный файл
331
server/enterprise/elasticsearch/opensearch/aggregation_job.go
Обычный файл
@@ -0,0 +1,331 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.enterprise for license information.
|
||||
|
||||
package opensearch
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/opensearch-project/opensearch-go/v4/opensearchapi"
|
||||
|
||||
"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/app"
|
||||
"github.com/mattermost/mattermost/server/v8/channels/jobs"
|
||||
"github.com/mattermost/mattermost/server/v8/channels/store"
|
||||
"github.com/mattermost/mattermost/server/v8/enterprise/elasticsearch/common"
|
||||
"github.com/mattermost/mattermost/server/v8/platform/shared/filestore"
|
||||
)
|
||||
|
||||
const (
|
||||
aggregatorJobPollingInterval = 15 * time.Second
|
||||
indexDeletionBatchSize = 20
|
||||
)
|
||||
|
||||
type OpensearchAggregatorInterfaceImpl struct {
|
||||
Server *app.Server
|
||||
}
|
||||
|
||||
type OpensearchAggregatorWorker struct {
|
||||
name string
|
||||
// stateMut protects stopCh and stopped and helps enforce
|
||||
// ordering in case subsequent Run or Stop calls are made.
|
||||
stateMut sync.Mutex
|
||||
stopCh chan struct{}
|
||||
stopped bool
|
||||
stoppedCh chan bool
|
||||
jobs chan model.Job
|
||||
jobServer *jobs.JobServer
|
||||
logger mlog.LoggerIFace
|
||||
fileBackend filestore.FileBackend
|
||||
|
||||
client *opensearchapi.Client
|
||||
license func() *model.License
|
||||
}
|
||||
|
||||
func (esi *OpensearchAggregatorInterfaceImpl) MakeWorker() model.Worker {
|
||||
const workerName = "EnterpriseOpensearchAggregator"
|
||||
worker := OpensearchAggregatorWorker{
|
||||
name: workerName,
|
||||
stoppedCh: make(chan bool, 1),
|
||||
jobs: make(chan model.Job),
|
||||
jobServer: esi.Server.Jobs,
|
||||
logger: esi.Server.Jobs.Logger().With(mlog.String("worker_name", workerName)),
|
||||
fileBackend: esi.Server.Platform().FileBackend(),
|
||||
license: esi.Server.License,
|
||||
stopped: true,
|
||||
}
|
||||
|
||||
return &worker
|
||||
}
|
||||
|
||||
func (worker *OpensearchAggregatorWorker) Run() {
|
||||
worker.stateMut.Lock()
|
||||
// We have to re-assign the stop channel again, because
|
||||
// it might happen that the job was restarted due to a config change.
|
||||
if worker.stopped {
|
||||
worker.stopped = false
|
||||
worker.stopCh = make(chan struct{})
|
||||
} else {
|
||||
worker.stateMut.Unlock()
|
||||
return
|
||||
}
|
||||
// Run is called from a separate goroutine and doesn't return.
|
||||
// So we cannot Unlock in a defer clause.
|
||||
worker.stateMut.Unlock()
|
||||
|
||||
worker.logger.Debug("Worker Started")
|
||||
|
||||
defer func() {
|
||||
worker.logger.Debug("Worker Finished")
|
||||
worker.stoppedCh <- true
|
||||
}()
|
||||
|
||||
client, err := createClient(worker.logger, worker.jobServer.Config(), worker.fileBackend, false)
|
||||
if err != nil {
|
||||
worker.logger.Error("Worker Failed to Create Client", mlog.Err(err))
|
||||
return
|
||||
}
|
||||
|
||||
worker.client = client
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-worker.stopCh:
|
||||
worker.logger.Debug("Worker Received stop signal")
|
||||
return
|
||||
case job := <-worker.jobs:
|
||||
worker.DoJob(&job)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (worker *OpensearchAggregatorWorker) IsEnabled(cfg *model.Config) bool {
|
||||
if license := worker.license(); license == nil || !*license.Features.Elasticsearch {
|
||||
return false
|
||||
}
|
||||
|
||||
if *cfg.ElasticsearchSettings.EnableIndexing {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func (worker *OpensearchAggregatorWorker) Stop() {
|
||||
worker.stateMut.Lock()
|
||||
defer worker.stateMut.Unlock()
|
||||
|
||||
// Set to close, and if already closed before, then return.
|
||||
if worker.stopped {
|
||||
return
|
||||
}
|
||||
worker.stopped = true
|
||||
|
||||
worker.logger.Debug("Worker Stopping")
|
||||
close(worker.stopCh)
|
||||
<-worker.stoppedCh
|
||||
}
|
||||
|
||||
func (worker *OpensearchAggregatorWorker) JobChannel() chan<- model.Job {
|
||||
return worker.jobs
|
||||
}
|
||||
|
||||
func (worker *OpensearchAggregatorWorker) DoJob(job *model.Job) {
|
||||
logger := worker.logger.With(jobs.JobLoggerFields(job)...)
|
||||
logger.Debug("Worker: Received a new candidate job.")
|
||||
defer worker.jobServer.HandleJobPanic(logger, job)
|
||||
|
||||
claimed, appErr := worker.jobServer.ClaimJob(job)
|
||||
if appErr != nil {
|
||||
logger.Warn("Worker: Error occurred while trying to claim job", mlog.Err(appErr))
|
||||
return
|
||||
}
|
||||
|
||||
if !claimed {
|
||||
return
|
||||
}
|
||||
|
||||
logger.Info("Worker: Aggregation job claimed by worker")
|
||||
|
||||
var cancelContext request.CTX = request.EmptyContext(worker.logger)
|
||||
cancelCtx, cancelCancelWatcher := context.WithCancel(context.Background())
|
||||
cancelWatcherChan := make(chan struct{}, 1)
|
||||
cancelContext = cancelContext.WithContext(cancelCtx)
|
||||
go worker.jobServer.CancellationWatcher(cancelContext, job.Id, cancelWatcherChan)
|
||||
defer cancelCancelWatcher()
|
||||
|
||||
rctx := request.EmptyContext(worker.logger)
|
||||
now := time.Now()
|
||||
today := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, time.Local)
|
||||
cutoff := today.AddDate(0, 0, -*worker.jobServer.Config().ElasticsearchSettings.AggregatePostsAfterDays+1)
|
||||
|
||||
// Get all the daily Elasticsearch post indexes to work out which days aren't aggregated yet.
|
||||
dateFormat := *worker.jobServer.Config().ElasticsearchSettings.IndexPrefix + common.IndexBasePosts + "_2006_01_02"
|
||||
datedIndexes := []time.Time{}
|
||||
|
||||
postIndexesResult, err := worker.client.Indices.Get(rctx.Context(), opensearchapi.IndicesGetReq{
|
||||
Indices: []string{*worker.jobServer.Config().ElasticsearchSettings.IndexPrefix + common.IndexBasePosts + "_*"},
|
||||
})
|
||||
if err != nil {
|
||||
appError := model.NewAppError("OpensearchAggregatorWorker", "ent.elasticsearch.aggregator_worker.get_indexes.error", nil, "", http.StatusInternalServerError).Wrap(err)
|
||||
worker.setJobError(logger, job, appError)
|
||||
return
|
||||
}
|
||||
|
||||
for index := range postIndexesResult.Indices {
|
||||
var indexDate time.Time
|
||||
indexDate, err = time.Parse(dateFormat, index)
|
||||
if err != nil {
|
||||
logger.Warn("Failed to parse date from posts index. Ignoring index.", mlog.String("index", index))
|
||||
} else {
|
||||
datedIndexes = append(datedIndexes, indexDate)
|
||||
}
|
||||
}
|
||||
|
||||
// Work out how far back the reindexing (and index deletion) needs to go.
|
||||
var oldestDay time.Time
|
||||
oldestDayFound := false
|
||||
indexesToPurge := []string{}
|
||||
for _, date := range datedIndexes {
|
||||
if date.Before(cutoff) {
|
||||
logger.Debug("Worker: Post index identified for purging", mlog.Time("date", date))
|
||||
indexesToPurge = append(indexesToPurge, date.Format(dateFormat))
|
||||
if !oldestDayFound || oldestDay.After(date) {
|
||||
oldestDay = date
|
||||
oldestDayFound = true
|
||||
}
|
||||
} else {
|
||||
logger.Debug("Worker: Post index is within the range to keep", mlog.Time("date", date))
|
||||
}
|
||||
}
|
||||
|
||||
if !oldestDayFound {
|
||||
// Nothing to purge.
|
||||
logger.Info("Worker: Aggregation job completed. Nothing to aggregate.")
|
||||
worker.setJobSuccess(logger, job)
|
||||
return
|
||||
}
|
||||
|
||||
// Trigger a reindexing job with the appropriate dates.
|
||||
reindexingStartDate := oldestDay
|
||||
reindexingEndDate := cutoff
|
||||
|
||||
logger.Info("Worker: Aggregation job reindexing", mlog.String("start_date", reindexingStartDate.Format("2006-01-02")), mlog.String("end_date", reindexingEndDate.Format("2006-01-02")))
|
||||
|
||||
var indexJob *model.Job
|
||||
if indexJob, appErr = worker.jobServer.CreateJob(
|
||||
rctx,
|
||||
model.JobTypeElasticsearchPostIndexing,
|
||||
map[string]string{
|
||||
"start_time": strconv.FormatInt(reindexingStartDate.UnixNano()/int64(time.Millisecond), 10),
|
||||
"end_time": strconv.FormatInt(reindexingEndDate.UnixNano()/int64(time.Millisecond), 10),
|
||||
},
|
||||
); appErr != nil {
|
||||
logger.Error("Worker: Failed to create indexing job.", mlog.Err(appErr))
|
||||
appError := model.NewAppError("OpensearchAggregatorWorker", "ent.elasticsearch.aggregator_worker.create_index_job.error", nil, "", http.StatusInternalServerError).Wrap(appErr)
|
||||
worker.setJobError(logger, job, appError)
|
||||
return
|
||||
}
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-cancelWatcherChan:
|
||||
logger.Info("Worker: Aggregation job has been canceled via CancellationWatcher")
|
||||
worker.setJobCanceled(logger, job)
|
||||
return
|
||||
|
||||
case <-worker.stopCh:
|
||||
logger.Info("Worker: Aggregation job has been canceled via Worker Stop")
|
||||
worker.setJobCanceled(logger, job)
|
||||
return
|
||||
|
||||
case <-time.After(aggregatorJobPollingInterval):
|
||||
// Get the details of the indexing job we are waiting on.
|
||||
indexJob, err = worker.jobServer.Store.Job().Get(rctx, indexJob.Id)
|
||||
if err != nil {
|
||||
var appErr *model.AppError
|
||||
var nfErr *store.ErrNotFound
|
||||
switch {
|
||||
case errors.As(err, &nfErr):
|
||||
appErr = model.NewAppError("DoJob", "app.job.get.app_error", nil, "", http.StatusNotFound).Wrap(nfErr)
|
||||
default:
|
||||
appErr = model.NewAppError("DoJob", "app.job.get.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
||||
}
|
||||
worker.setJobError(logger, job, appErr)
|
||||
return
|
||||
}
|
||||
|
||||
// Wait for the aggregation job to finish.
|
||||
// On success, we delete the old indexes.
|
||||
// Otherwise, fail the job.
|
||||
switch indexJob.Status {
|
||||
case model.JobStatusSuccess:
|
||||
// We limit the number of indexes to delete at one shot.
|
||||
// A minor side-effect of this is that the aggregation job status
|
||||
// will be redundantly queried multiple times, but that's not a major bottleneck.
|
||||
curWindow := indexesToPurge
|
||||
deleteMore := false
|
||||
if len(indexesToPurge) > indexDeletionBatchSize {
|
||||
curWindow = indexesToPurge[:indexDeletionBatchSize]
|
||||
indexesToPurge = indexesToPurge[indexDeletionBatchSize:]
|
||||
deleteMore = true
|
||||
}
|
||||
// Delete indexes
|
||||
if _, err = worker.client.Indices.Delete(rctx.Context(), opensearchapi.IndicesDeleteReq{
|
||||
Indices: curWindow,
|
||||
}); err != nil {
|
||||
appError := model.NewAppError("OpensearchAggregatorWorker", "ent.elasticsearch.aggregator_worker.delete_indexes.error", nil, "", http.StatusInternalServerError).Wrap(err)
|
||||
logger.Error("Worker: Failed to delete indexes for job", mlog.String("workername", worker.name), mlog.String("job_id", job.Id), mlog.Err(appError))
|
||||
worker.setJobError(logger, job, appError)
|
||||
return
|
||||
}
|
||||
|
||||
if !deleteMore {
|
||||
// Job done. Set the status to success.
|
||||
logger.Info("Worker: Aggregation job finished successfully")
|
||||
worker.setJobSuccess(logger, job)
|
||||
return
|
||||
}
|
||||
case model.JobStatusPending, model.JobStatusInProgress:
|
||||
// Indexing job is in progress or pending. Update the progress of this job.
|
||||
if err := worker.jobServer.SetJobProgress(job, indexJob.Progress); err != nil {
|
||||
logger.Error("Worker: Failed to set progress for job", mlog.Err(err))
|
||||
worker.setJobError(logger, job, err)
|
||||
return
|
||||
}
|
||||
default:
|
||||
// error case
|
||||
appError := model.NewAppError("OpensearchAggregatorWorker", "ent.elasticsearch.aggregator_worker.index_job_failed.error", nil, "", http.StatusInternalServerError)
|
||||
logger.Error("Worker: Index aggregation job failed", mlog.Err(appError))
|
||||
worker.setJobError(logger, job, appError)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (worker *OpensearchAggregatorWorker) setJobSuccess(logger mlog.LoggerIFace, job *model.Job) {
|
||||
if err := worker.jobServer.SetJobSuccess(job); err != nil {
|
||||
logger.Error("Worker: Failed to set success for job", mlog.Err(err))
|
||||
worker.setJobError(logger, job, err)
|
||||
}
|
||||
}
|
||||
|
||||
func (worker *OpensearchAggregatorWorker) setJobError(logger mlog.LoggerIFace, job *model.Job, appError *model.AppError) {
|
||||
if err := worker.jobServer.SetJobError(job, appError); err != nil {
|
||||
logger.Error("Worker: Failed to set job error", mlog.Err(err))
|
||||
}
|
||||
}
|
||||
|
||||
func (worker *OpensearchAggregatorWorker) setJobCanceled(logger mlog.LoggerIFace, job *model.Job) {
|
||||
if err := worker.jobServer.SetJobCanceled(job); err != nil {
|
||||
logger.Error("Worker: Failed to mark job as canceled", mlog.Err(err))
|
||||
}
|
||||
}
|
||||
223
server/enterprise/elasticsearch/opensearch/aggregation_job_test.go
Обычный файл
223
server/enterprise/elasticsearch/opensearch/aggregation_job_test.go
Обычный файл
@@ -0,0 +1,223 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.enterprise for license information.
|
||||
|
||||
package opensearch
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/opensearch-project/opensearch-go/v4/opensearchapi"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/mock"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/mattermost/mattermost/server/public/model"
|
||||
"github.com/mattermost/mattermost/server/public/shared/request"
|
||||
"github.com/mattermost/mattermost/server/v8/channels/api4"
|
||||
"github.com/mattermost/mattermost/server/v8/channels/store/storetest/mocks"
|
||||
"github.com/mattermost/mattermost/server/v8/enterprise/elasticsearch/common"
|
||||
)
|
||||
|
||||
func TestElasticsearchAggregation(t *testing.T) {
|
||||
if os.Getenv("IS_CI") == "true" {
|
||||
os.Setenv("MM_ELASTICSEARCHSETTINGS_CONNECTIONURL", "http://opensearch:9201")
|
||||
os.Setenv("MM_ELASTICSEARCHSETTINGS_BACKEND", "opensearch")
|
||||
}
|
||||
|
||||
defer func() {
|
||||
if os.Getenv("IS_CI") == "true" {
|
||||
os.Setenv("MM_ELASTICSEARCHSETTINGS_CONNECTIONURL", "http://elasticsearch:9201")
|
||||
os.Unsetenv("MM_ELASTICSEARCHSETTINGS_BACKEND")
|
||||
}
|
||||
}()
|
||||
|
||||
th := api4.SetupEnterpriseWithStoreMock(t)
|
||||
rctx := request.TestContext(t)
|
||||
|
||||
mockUserStore := mocks.UserStore{}
|
||||
mockUserStore.On("Count", mock.Anything).Return(int64(10), nil)
|
||||
mockUserStore.On("GetAllProfiles", mock.Anything).Return(nil, nil)
|
||||
|
||||
mockPostStore := mocks.PostStore{}
|
||||
mockPostStore.On("GetMaxPostSize").Return(65535, nil)
|
||||
|
||||
mockSystemStore := mocks.SystemStore{}
|
||||
mockSystemStore.On("GetByName", "UpgradedFromTE").Return(&model.System{Name: "UpgradedFromTE", Value: "false"}, nil)
|
||||
mockSystemStore.On("GetByName", "InstallationDate").Return(&model.System{Name: "InstallationDate", Value: "10"}, nil)
|
||||
mockSystemStore.On("GetByName", "FirstServerRunTimestamp").Return(&model.System{Name: "FirstServerRunTimestamp", Value: "10"}, nil)
|
||||
|
||||
mockJobStore := mocks.JobStore{}
|
||||
mockJobStore.On("Save", mock.AnythingOfType("*model.Job")).Return(&model.Job{}, nil)
|
||||
mockJobStore.On("UpdateStatus", mock.AnythingOfType("string"), model.JobStatusSuccess).Return(&model.Job{}, nil)
|
||||
mockJobStore.On("Get", mock.AnythingOfType("*request.Context"), mock.AnythingOfType("string")).Return(&model.Job{
|
||||
Status: model.JobStatusSuccess,
|
||||
}, nil)
|
||||
mockJobStore.On("UpdateStatusOptimistically",
|
||||
mock.AnythingOfType("string"),
|
||||
model.JobStatusPending,
|
||||
model.JobStatusInProgress).Return(true, nil)
|
||||
mockJobStore.On("GetAllByType", mock.AnythingOfType("string")).Return([]*model.Job{{
|
||||
Id: "abcxyz123",
|
||||
Type: "EnterpriseElasticsearchIndexer",
|
||||
Status: model.JobStatusCanceled,
|
||||
}}, nil)
|
||||
|
||||
mockStore := th.App.Srv().Platform().Store.(*mocks.Store)
|
||||
mockStore.On("User").Return(&mockUserStore)
|
||||
mockStore.On("Post").Return(&mockPostStore)
|
||||
mockStore.On("System").Return(&mockSystemStore)
|
||||
mockStore.On("Job").Return(&mockJobStore)
|
||||
mockStore.On("GetDBSchemaVersion").Return(1, nil)
|
||||
|
||||
aggImpl := OpensearchAggregatorInterfaceImpl{Server: th.Server}
|
||||
|
||||
// Register search engine
|
||||
th.App.SearchEngine().RegisterElasticsearchEngine(&OpensearchInterfaceImpl{
|
||||
Platform: th.Server.Platform(),
|
||||
})
|
||||
|
||||
// Set up the state for the tests.
|
||||
th.App.UpdateConfig(func(cfg *model.Config) {
|
||||
if os.Getenv("IS_CI") == "true" {
|
||||
*cfg.ElasticsearchSettings.ConnectionURL = "http://opensearch:9201"
|
||||
} else {
|
||||
*cfg.ElasticsearchSettings.ConnectionURL = "http://localhost:9201"
|
||||
}
|
||||
*cfg.ElasticsearchSettings.Backend = model.ElasticsearchSettingsOSBackend
|
||||
*cfg.ElasticsearchSettings.EnableIndexing = true
|
||||
*cfg.ElasticsearchSettings.EnableSearching = true
|
||||
*cfg.ElasticsearchSettings.EnableAutocomplete = true
|
||||
*cfg.ElasticsearchSettings.LiveIndexingBatchSize = 1
|
||||
*cfg.ElasticsearchSettings.AggregatePostsAfterDays = 1
|
||||
*cfg.SqlSettings.DisableDatabaseSearch = true
|
||||
})
|
||||
|
||||
esImpl := th.App.SearchEngine().ElasticsearchEngine
|
||||
appErr := esImpl.Start()
|
||||
if appErr != nil && appErr.Id != "ent.elasticsearch.start.already_started.app_error" {
|
||||
require.Fail(t, "failed to start elasticsearch", appErr)
|
||||
}
|
||||
require.Nil(t, esImpl.PurgeIndexes(rctx))
|
||||
|
||||
post := &model.Post{
|
||||
Id: model.NewId(),
|
||||
ChannelId: "channel",
|
||||
Message: "hi",
|
||||
}
|
||||
for i := 0; i < indexDeletionBatchSize+1; i++ {
|
||||
indexPost(t, th, esImpl.(*OpensearchInterfaceImpl),
|
||||
post,
|
||||
time.Now().Add(-time.Duration(4+i)*24*time.Hour))
|
||||
}
|
||||
|
||||
job := &model.Job{
|
||||
Id: model.NewId(),
|
||||
Type: model.JobTypeElasticsearchPostAggregation,
|
||||
Status: model.JobStatusPending,
|
||||
}
|
||||
|
||||
_, err := th.Server.Store().Job().Save(job)
|
||||
require.NoError(t, err)
|
||||
|
||||
worker := aggImpl.MakeWorker().(*OpensearchAggregatorWorker)
|
||||
worker.client = createTestClient(t, th.Context, th.App.Config(), th.App.FileBackend())
|
||||
worker.jobServer.Store = mockStore
|
||||
|
||||
indexingImpl := OpensearchIndexerInterfaceImpl{
|
||||
Server: th.App.Srv(),
|
||||
}
|
||||
th.Server.Jobs.RegisterJobType(model.JobTypeElasticsearchPostIndexing, indexingImpl.MakeWorker(), nil)
|
||||
|
||||
worker.DoJob(job)
|
||||
|
||||
// We assert the minimum number of calls to verify that
|
||||
// batching is working correctly. Because job().Get() will happen
|
||||
// in each iteration.
|
||||
numCalls := 0
|
||||
for _, call := range mockJobStore.Calls {
|
||||
if call.Method == "Get" {
|
||||
numCalls++
|
||||
}
|
||||
}
|
||||
assert.GreaterOrEqual(t, numCalls, 8, "Unexpected number of Jobstore.Get calls")
|
||||
}
|
||||
|
||||
func TestElasticsearchAggregationSkipDuringBulkIndexing(t *testing.T) {
|
||||
th := api4.SetupEnterpriseWithStoreMock(t)
|
||||
|
||||
mockUserStore := mocks.UserStore{}
|
||||
mockUserStore.On("Count", mock.Anything).Return(int64(10), nil)
|
||||
|
||||
mockPostStore := mocks.PostStore{}
|
||||
mockPostStore.On("GetMaxPostSize").Return(65535, nil)
|
||||
|
||||
mockSystemStore := mocks.SystemStore{}
|
||||
mockSystemStore.On("GetByName", "UpgradedFromTE").Return(&model.System{Name: "UpgradedFromTE", Value: "false"}, nil)
|
||||
mockSystemStore.On("GetByName", "InstallationDate").Return(&model.System{Name: "InstallationDate", Value: "10"}, nil)
|
||||
mockSystemStore.On("GetByName", "FirstServerRunTimestamp").Return(&model.System{Name: "FirstServerRunTimestamp", Value: "10"}, nil)
|
||||
|
||||
mockJobStore := mocks.JobStore{}
|
||||
|
||||
mockStore := th.App.Srv().Platform().Store.(*mocks.Store)
|
||||
mockStore.On("User").Return(&mockUserStore)
|
||||
mockStore.On("Post").Return(&mockPostStore)
|
||||
mockStore.On("System").Return(&mockSystemStore)
|
||||
mockStore.On("Job").Return(&mockJobStore)
|
||||
mockStore.On("GetDBSchemaVersion").Return(1, nil)
|
||||
|
||||
aggImpl := OpensearchAggregatorInterfaceImpl{Server: th.Server}
|
||||
aggImpl.Server.Jobs.Store = mockStore
|
||||
|
||||
// Register search engine
|
||||
th.App.SearchEngine().RegisterElasticsearchEngine(&OpensearchInterfaceImpl{
|
||||
Platform: th.Server.Platform(),
|
||||
})
|
||||
|
||||
// Set up the state for the tests.
|
||||
th.App.UpdateConfig(func(cfg *model.Config) {
|
||||
*cfg.ElasticsearchSettings.EnableIndexing = true
|
||||
*cfg.ElasticsearchSettings.EnableSearching = true
|
||||
*cfg.ElasticsearchSettings.EnableAutocomplete = true
|
||||
*cfg.ElasticsearchSettings.LiveIndexingBatchSize = 1
|
||||
*cfg.ElasticsearchSettings.AggregatePostsAfterDays = 1
|
||||
*cfg.SqlSettings.DisableDatabaseSearch = true
|
||||
})
|
||||
|
||||
sched := aggImpl.MakeScheduler()
|
||||
// Pass pending jobs as true
|
||||
job, appErr := sched.ScheduleJob(th.Context, th.App.Config(), true, nil)
|
||||
require.Nil(t, job)
|
||||
require.Nil(t, appErr)
|
||||
|
||||
mockJobStore.AssertNotCalled(t, "GetCountByStatusAndType")
|
||||
}
|
||||
|
||||
func indexPost(t *testing.T, th *api4.TestHelper, esImpl *OpensearchInterfaceImpl, post *model.Post, createTime time.Time) { //nolint:unused
|
||||
t.Helper()
|
||||
indexName := common.BuildPostIndexName(*th.Server.Config().ElasticsearchSettings.AggregatePostsAfterDays,
|
||||
common.IndexBasePosts,
|
||||
common.IndexBasePosts_MONTH,
|
||||
createTime.Add(-1*24*time.Hour),
|
||||
model.GetMillisForTime(createTime),
|
||||
)
|
||||
searchPost, err := common.ESPostFromPost(post, "teamID")
|
||||
require.NoError(t, err)
|
||||
ctx, cancel := context.WithTimeout(context.Background(),
|
||||
time.Duration(*esImpl.Platform.Config().ElasticsearchSettings.RequestTimeoutSeconds)*time.Second)
|
||||
defer cancel()
|
||||
|
||||
buf, err := json.Marshal(searchPost)
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = esImpl.client.Index(ctx, opensearchapi.IndexReq{
|
||||
Index: indexName,
|
||||
DocumentID: post.Id,
|
||||
Body: bytes.NewReader(buf),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.enterprise for license information.
|
||||
|
||||
package opensearch
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"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/app"
|
||||
"github.com/mattermost/mattermost/server/v8/channels/jobs"
|
||||
ejobs "github.com/mattermost/mattermost/server/v8/einterfaces/jobs"
|
||||
)
|
||||
|
||||
type OpenSearchAggregatorScheduler struct {
|
||||
jobServer *jobs.JobServer
|
||||
server *app.Server
|
||||
}
|
||||
|
||||
func (s *OpenSearchAggregatorScheduler) Enabled(cfg *model.Config) bool {
|
||||
if license := s.server.License(); license == nil || !*license.Features.Elasticsearch {
|
||||
return false
|
||||
}
|
||||
|
||||
if *cfg.ElasticsearchSettings.EnableIndexing {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func (s *OpenSearchAggregatorScheduler) NextScheduleTime(cfg *model.Config, now time.Time, pendingJobs bool, lastSuccessfulJob *model.Job) *time.Time {
|
||||
parsedTime, err := time.Parse("15:04", *cfg.ElasticsearchSettings.PostsAggregatorJobStartTime)
|
||||
if err != nil {
|
||||
s.server.Log().Error("Cannot determine next schedule time for opensearch post aggregator. PostsAggregatorJobStartTime config value is invalid.", mlog.Err(err))
|
||||
return nil
|
||||
}
|
||||
|
||||
return jobs.GenerateNextStartDateTime(now, parsedTime)
|
||||
}
|
||||
|
||||
func (s *OpenSearchAggregatorScheduler) ScheduleJob(rctx request.CTX, _ *model.Config, pendingJobs bool, _ *model.Job) (*model.Job, *model.AppError) {
|
||||
if pendingJobs {
|
||||
s.server.Log().Warn("An aggregator job is already running. Skipping.")
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// Don't schedule a job if we already have a running bulk indexing job
|
||||
count, err := s.jobServer.Store.Job().GetCountByStatusAndType(model.JobStatusInProgress, model.JobTypeElasticsearchPostIndexing)
|
||||
if err != nil {
|
||||
return nil, model.NewAppError(
|
||||
"ScheduleJob",
|
||||
model.NoTranslation,
|
||||
nil,
|
||||
"",
|
||||
http.StatusInternalServerError).Wrap(err)
|
||||
}
|
||||
if count > 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
return s.jobServer.CreateJob(rctx, model.JobTypeElasticsearchPostAggregation, nil)
|
||||
}
|
||||
|
||||
func (esi *OpensearchAggregatorInterfaceImpl) MakeScheduler() ejobs.Scheduler {
|
||||
return &OpenSearchAggregatorScheduler{
|
||||
server: esi.Server,
|
||||
jobServer: esi.Server.Jobs,
|
||||
}
|
||||
}
|
||||
162
server/enterprise/elasticsearch/opensearch/bulk.go
Обычный файл
162
server/enterprise/elasticsearch/opensearch/bulk.go
Обычный файл
@@ -0,0 +1,162 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.enterprise for license information.
|
||||
|
||||
package opensearch
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/elastic/go-elasticsearch/v8/typedapi/types"
|
||||
"github.com/mattermost/mattermost/server/public/model"
|
||||
"github.com/mattermost/mattermost/server/public/shared/mlog"
|
||||
"github.com/mattermost/mattermost/server/v8/enterprise/elasticsearch/common"
|
||||
"github.com/opensearch-project/opensearch-go/v4/opensearchapi"
|
||||
)
|
||||
|
||||
type Bulk struct {
|
||||
mut sync.Mutex
|
||||
buf *bytes.Buffer
|
||||
|
||||
logger mlog.LoggerIFace
|
||||
client *opensearchapi.Client
|
||||
settings model.ElasticsearchSettings
|
||||
|
||||
quitFlusher chan struct{}
|
||||
quitFlusherWg sync.WaitGroup
|
||||
|
||||
pendingRequests int
|
||||
}
|
||||
|
||||
func NewBulk(settings model.ElasticsearchSettings,
|
||||
logger mlog.LoggerIFace,
|
||||
client *opensearchapi.Client) *Bulk {
|
||||
b := &Bulk{
|
||||
settings: settings,
|
||||
logger: logger,
|
||||
client: client,
|
||||
quitFlusher: make(chan struct{}),
|
||||
buf: &bytes.Buffer{},
|
||||
}
|
||||
|
||||
b.quitFlusherWg.Add(1)
|
||||
go b.periodicFlusher()
|
||||
|
||||
return b
|
||||
}
|
||||
|
||||
// IndexOp is a helper function to add an IndexOperation to the current bulk request.
|
||||
// doc argument can be a []byte, json.RawMessage or a struct.
|
||||
func (r *Bulk) IndexOp(op *types.IndexOperation, doc any) error {
|
||||
r.mut.Lock()
|
||||
defer r.mut.Unlock()
|
||||
|
||||
operation := types.OperationContainer{Index: op}
|
||||
header, err := json.Marshal(operation)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
r.buf.Write(header)
|
||||
r.buf.Write([]byte("\n"))
|
||||
|
||||
switch v := doc.(type) {
|
||||
case []byte:
|
||||
r.buf.Write(v)
|
||||
case json.RawMessage:
|
||||
r.buf.Write(v)
|
||||
default:
|
||||
body, err := json.Marshal(doc)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
r.buf.Write(body)
|
||||
}
|
||||
|
||||
r.buf.Write([]byte("\n"))
|
||||
|
||||
return r.flushIfNecessary()
|
||||
}
|
||||
|
||||
// DeleteOp is a helper function to add a DeleteOperation to the current bulk request.
|
||||
func (r *Bulk) DeleteOp(op *types.DeleteOperation) error {
|
||||
r.mut.Lock()
|
||||
defer r.mut.Unlock()
|
||||
|
||||
operation := types.OperationContainer{Delete: op}
|
||||
header, err := json.Marshal(operation)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
r.buf.Write(header)
|
||||
r.buf.Write([]byte("\n"))
|
||||
|
||||
return r.flushIfNecessary()
|
||||
}
|
||||
|
||||
// flushIfNecessary flushes the pending buffer if needed.
|
||||
// It MUST be called with an already acquired mutex.
|
||||
func (r *Bulk) flushIfNecessary() error {
|
||||
r.pendingRequests++
|
||||
|
||||
if r.pendingRequests > *r.settings.LiveIndexingBatchSize {
|
||||
return r._flush()
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *Bulk) Stop() error {
|
||||
r.mut.Lock()
|
||||
defer r.mut.Unlock()
|
||||
r.logger.Info("Stopping Bulk processor")
|
||||
|
||||
if r.pendingRequests > 0 {
|
||||
return r._flush()
|
||||
}
|
||||
|
||||
close(r.quitFlusher)
|
||||
r.quitFlusherWg.Wait()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *Bulk) periodicFlusher() {
|
||||
defer r.quitFlusherWg.Done()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-time.After(common.BulkFlushInterval):
|
||||
r.mut.Lock()
|
||||
if r.pendingRequests > 0 {
|
||||
if err := r._flush(); err != nil {
|
||||
r.logger.Warn("Error flushing live indexing buffer", mlog.Err(err))
|
||||
}
|
||||
}
|
||||
r.mut.Unlock()
|
||||
case <-r.quitFlusher:
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// _flush MUST be called with an acquired lock.
|
||||
func (r *Bulk) _flush() error {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Duration(*r.settings.RequestTimeoutSeconds)*time.Second)
|
||||
defer cancel()
|
||||
|
||||
_, err := r.client.Bulk(ctx, opensearchapi.BulkReq{
|
||||
Body: bytes.NewReader(r.buf.Bytes()),
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
r.buf.Reset()
|
||||
r.pendingRequests = 0
|
||||
|
||||
return nil
|
||||
}
|
||||
68
server/enterprise/elasticsearch/opensearch/bulk_test.go
Обычный файл
68
server/enterprise/elasticsearch/opensearch/bulk_test.go
Обычный файл
@@ -0,0 +1,68 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.enterprise for license information.
|
||||
|
||||
package opensearch
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/elastic/go-elasticsearch/v8/typedapi/types"
|
||||
"github.com/mattermost/mattermost/server/public/model"
|
||||
"github.com/mattermost/mattermost/server/v8/channels/api4"
|
||||
"github.com/mattermost/mattermost/server/v8/enterprise/elasticsearch/common"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestBulkProcessor(t *testing.T) {
|
||||
th := api4.SetupEnterprise(t)
|
||||
defer th.TearDown()
|
||||
|
||||
if os.Getenv("IS_CI") == "true" {
|
||||
os.Setenv("MM_ELASTICSEARCHSETTINGS_CONNECTIONURL", "http://opensearch:9201")
|
||||
os.Setenv("MM_ELASTICSEARCHSETTINGS_BACKEND", "opensearch")
|
||||
}
|
||||
|
||||
defer func() {
|
||||
if os.Getenv("IS_CI") == "true" {
|
||||
os.Setenv("MM_ELASTICSEARCHSETTINGS_CONNECTIONURL", "http://elasticsearch:9201")
|
||||
os.Unsetenv("MM_ELASTICSEARCHSETTINGS_BACKEND")
|
||||
}
|
||||
}()
|
||||
|
||||
th.App.UpdateConfig(func(cfg *model.Config) {
|
||||
if os.Getenv("IS_CI") == "true" {
|
||||
*cfg.ElasticsearchSettings.ConnectionURL = "http://opensearch:9201"
|
||||
} else {
|
||||
*cfg.ElasticsearchSettings.ConnectionURL = "http://localhost:9201"
|
||||
}
|
||||
*cfg.ElasticsearchSettings.Backend = model.ElasticsearchSettingsOSBackend
|
||||
*cfg.ElasticsearchSettings.EnableIndexing = true
|
||||
*cfg.ElasticsearchSettings.EnableSearching = true
|
||||
*cfg.ElasticsearchSettings.EnableAutocomplete = true
|
||||
})
|
||||
|
||||
client := createTestClient(t, th.Context, th.App.Config(), th.App.FileBackend())
|
||||
bulk := NewBulk(th.App.Config().ElasticsearchSettings,
|
||||
th.Server.Platform().Log(),
|
||||
client)
|
||||
|
||||
post, err := common.ESPostFromPost(&model.Post{
|
||||
Id: model.NewId(),
|
||||
Message: "hello world",
|
||||
}, "myteam")
|
||||
require.NoError(t, err)
|
||||
|
||||
err = bulk.IndexOp(&types.IndexOperation{
|
||||
Index_: model.NewPointer("myindex"),
|
||||
Id_: model.NewPointer(post.Id),
|
||||
}, post)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Equal(t, 1, bulk.pendingRequests)
|
||||
|
||||
err = bulk.Stop()
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Equal(t, 0, bulk.pendingRequests)
|
||||
}
|
||||
124
server/enterprise/elasticsearch/opensearch/common.go
Обычный файл
124
server/enterprise/elasticsearch/opensearch/common.go
Обычный файл
@@ -0,0 +1,124 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.enterprise for license information.
|
||||
|
||||
package opensearch
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/opensearch-project/opensearch-go/v4"
|
||||
"github.com/opensearch-project/opensearch-go/v4/opensearchapi"
|
||||
|
||||
"github.com/mattermost/mattermost/server/public/model"
|
||||
"github.com/mattermost/mattermost/server/public/shared/mlog"
|
||||
"github.com/mattermost/mattermost/server/v8/enterprise/elasticsearch/common"
|
||||
"github.com/mattermost/mattermost/server/v8/platform/shared/filestore"
|
||||
)
|
||||
|
||||
func createClient(logger mlog.LoggerIFace, cfg *model.Config, fileBackend filestore.FileBackend, debugLogging bool) (*opensearchapi.Client, *model.AppError) {
|
||||
esCfg, appErr := createClientConfig(logger, cfg, fileBackend, debugLogging)
|
||||
if appErr != nil {
|
||||
return nil, appErr
|
||||
}
|
||||
|
||||
client, err := opensearchapi.NewClient(*esCfg)
|
||||
if err != nil {
|
||||
return nil, model.NewAppError("Elasticsearch.createClient", "ent.elasticsearch.create_client.connect_failed", nil, "", http.StatusInternalServerError).Wrap(err)
|
||||
}
|
||||
|
||||
return client, nil
|
||||
}
|
||||
|
||||
func createClientConfig(logger mlog.LoggerIFace, cfg *model.Config, fileBackend filestore.FileBackend, debugLogging bool) (*opensearchapi.Config, *model.AppError) {
|
||||
tp := http.DefaultTransport.(*http.Transport).Clone()
|
||||
tp.TLSClientConfig = &tls.Config{
|
||||
InsecureSkipVerify: *cfg.ElasticsearchSettings.SkipTLSVerification,
|
||||
}
|
||||
|
||||
osCfg := &opensearchapi.Config{
|
||||
Client: opensearch.Config{
|
||||
Addresses: []string{*cfg.ElasticsearchSettings.ConnectionURL},
|
||||
RetryBackoff: func(i int) time.Duration { return time.Duration(i) * 100 * time.Millisecond }, // A minimal backoff function
|
||||
RetryOnStatus: []int{502, 503, 504, 429}, // Retry on 429 TooManyRequests statuses
|
||||
MaxRetries: 3,
|
||||
DiscoverNodesOnStart: *cfg.ElasticsearchSettings.Sniff,
|
||||
},
|
||||
}
|
||||
|
||||
if osCfg.Client.DiscoverNodesOnStart {
|
||||
osCfg.Client.DiscoverNodesInterval = 30 * time.Second
|
||||
}
|
||||
|
||||
if *cfg.ElasticsearchSettings.ClientCert != "" {
|
||||
appErr := configureClientCertificate(tp.TLSClientConfig, cfg, fileBackend)
|
||||
if appErr != nil {
|
||||
return nil, appErr
|
||||
}
|
||||
}
|
||||
|
||||
// custom CA
|
||||
if *cfg.ElasticsearchSettings.CA != "" {
|
||||
appErr := configureCA(&osCfg.Client, cfg, fileBackend)
|
||||
if appErr != nil {
|
||||
return nil, appErr
|
||||
}
|
||||
}
|
||||
|
||||
osCfg.Client.Transport = tp
|
||||
|
||||
if *cfg.ElasticsearchSettings.Username != "" {
|
||||
osCfg.Client.Username = *cfg.ElasticsearchSettings.Username
|
||||
osCfg.Client.Password = *cfg.ElasticsearchSettings.Password
|
||||
}
|
||||
|
||||
// This is a compatibility mode from previous config settings.
|
||||
// We have to conditionally enable debug logging due to
|
||||
// https://github.com/elastic/elastic-transport-go/issues/22
|
||||
// Although, this is opensearch, the issue is the same.
|
||||
if *cfg.ElasticsearchSettings.Trace == "all" && debugLogging {
|
||||
osCfg.Client.EnableDebugLogger = true
|
||||
}
|
||||
|
||||
osCfg.Client.Logger = common.NewLogger("Opensearch", logger, *cfg.ElasticsearchSettings.Trace == "all")
|
||||
|
||||
return osCfg, nil
|
||||
}
|
||||
|
||||
func configureCA(esCfg *opensearch.Config, cfg *model.Config, fb filestore.FileBackend) *model.AppError {
|
||||
// read the certificate authority (CA) file
|
||||
clientCA, err := common.ReadFileSafely(fb, *cfg.ElasticsearchSettings.CA)
|
||||
if err != nil {
|
||||
return model.NewAppError("Elasticsearch.createClient", "ent.elasticsearch.create_client.ca_cert_missing", nil, "", http.StatusInternalServerError).Wrap(err)
|
||||
}
|
||||
|
||||
esCfg.CACert = clientCA
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func configureClientCertificate(tlsConfig *tls.Config, cfg *model.Config, fb filestore.FileBackend) *model.AppError {
|
||||
// read the client certificate file
|
||||
clientCert, err := common.ReadFileSafely(fb, *cfg.ElasticsearchSettings.ClientCert)
|
||||
if err != nil {
|
||||
return model.NewAppError("Elasticsearch.createClient", "ent.elasticsearch.create_client.client_cert_missing", nil, "", http.StatusInternalServerError).Wrap(err)
|
||||
}
|
||||
|
||||
// read the client key file
|
||||
clientKey, err := common.ReadFileSafely(fb, *cfg.ElasticsearchSettings.ClientKey)
|
||||
if err != nil {
|
||||
return model.NewAppError("Elasticsearch.createClient", "ent.elasticsearch.create_client.client_key_missing", nil, "", http.StatusInternalServerError).Wrap(err)
|
||||
}
|
||||
|
||||
// load the client key and certificate
|
||||
certificate, err := tls.X509KeyPair(clientCert, clientKey)
|
||||
if err != nil {
|
||||
return model.NewAppError("Elasticsearch.createClient", "ent.elasticsearch.create_client.client_cert_malformed", nil, "", http.StatusInternalServerError).Wrap(err)
|
||||
}
|
||||
|
||||
// update the TLS config
|
||||
tlsConfig.Certificates = []tls.Certificate{certificate}
|
||||
|
||||
return nil
|
||||
}
|
||||
73
server/enterprise/elasticsearch/opensearch/indexing_job.go
Обычный файл
73
server/enterprise/elasticsearch/opensearch/indexing_job.go
Обычный файл
@@ -0,0 +1,73 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.enterprise for license information.
|
||||
|
||||
package opensearch
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"time"
|
||||
|
||||
"github.com/mattermost/mattermost/server/v8/enterprise/elasticsearch/common"
|
||||
|
||||
"github.com/opensearch-project/opensearch-go/v4/opensearchutil"
|
||||
|
||||
"github.com/mattermost/mattermost/server/public/model"
|
||||
"github.com/mattermost/mattermost/server/public/shared/mlog"
|
||||
"github.com/mattermost/mattermost/server/v8/channels/app"
|
||||
)
|
||||
|
||||
type OpensearchIndexerInterfaceImpl struct {
|
||||
Server *app.Server
|
||||
bulkProcessor opensearchutil.BulkIndexer
|
||||
}
|
||||
|
||||
func (esi *OpensearchIndexerInterfaceImpl) MakeWorker() model.Worker {
|
||||
const workerName = "EnterpriseOpensearchIndexer"
|
||||
|
||||
// Initializing logger
|
||||
logger := esi.Server.Jobs.Logger().With(mlog.String("worker_name", workerName))
|
||||
|
||||
// Creating the client
|
||||
client, appErr := createClient(logger, esi.Server.Jobs.Config(), esi.Server.Platform().FileBackend(), true)
|
||||
if appErr != nil {
|
||||
logger.Error("Worker: Failed to Create Client", mlog.Err(appErr))
|
||||
return nil
|
||||
}
|
||||
|
||||
return common.NewIndexerWorker(workerName,
|
||||
esi.Server.Jobs,
|
||||
logger,
|
||||
esi.Server.Platform().FileBackend(),
|
||||
esi.Server.License,
|
||||
func() error {
|
||||
// Creating the bulk indexer from the client.
|
||||
biCfg := opensearchutil.BulkIndexerConfig{
|
||||
Client: client,
|
||||
OnError: func(_ context.Context, err error) {
|
||||
logger.Error("Error from opensearch bulk indexer", mlog.Err(err))
|
||||
},
|
||||
Timeout: time.Duration(*esi.Server.Jobs.Config().ElasticsearchSettings.RequestTimeoutSeconds) * time.Second,
|
||||
NumWorkers: common.NumIndexWorkers(),
|
||||
}
|
||||
if *esi.Server.Jobs.Config().ElasticsearchSettings.Trace == "all" {
|
||||
biCfg.DebugLogger = common.NewBulkIndexerLogger(logger, workerName)
|
||||
}
|
||||
var err error
|
||||
esi.bulkProcessor, err = opensearchutil.NewBulkIndexer(biCfg)
|
||||
return err
|
||||
},
|
||||
// Function to add an item in the bulk processor
|
||||
func(indexName, indexOp, docID string, body io.ReadSeeker) error {
|
||||
return esi.bulkProcessor.Add(context.Background(), opensearchutil.BulkIndexerItem{
|
||||
Index: indexName,
|
||||
Action: indexOp,
|
||||
DocumentID: docID,
|
||||
Body: body,
|
||||
})
|
||||
},
|
||||
// Closing the bulk processor
|
||||
func() error {
|
||||
return esi.bulkProcessor.Close(context.Background())
|
||||
})
|
||||
}
|
||||
94
server/enterprise/elasticsearch/opensearch/indexing_job_test.go
Обычный файл
94
server/enterprise/elasticsearch/opensearch/indexing_job_test.go
Обычный файл
@@ -0,0 +1,94 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.enterprise for license information.
|
||||
|
||||
package opensearch
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/mattermost/mattermost/server/public/model"
|
||||
"github.com/mattermost/mattermost/server/v8/channels/api4"
|
||||
)
|
||||
|
||||
func TestOpenSearchIndexerJobIsEnabled(t *testing.T) {
|
||||
t.Run("ElasticSearch feature is enabled then job is enabled", func(t *testing.T) {
|
||||
th := api4.SetupEnterpriseWithStoreMock(t)
|
||||
defer th.TearDown()
|
||||
|
||||
th.Server.SetLicense(model.NewTestLicense("elastic_search"))
|
||||
|
||||
osImpl := &OpensearchIndexerInterfaceImpl{
|
||||
Server: th.Server,
|
||||
}
|
||||
worker := osImpl.MakeWorker()
|
||||
|
||||
config := &model.Config{
|
||||
ElasticsearchSettings: model.ElasticsearchSettings{
|
||||
EnableIndexing: model.NewPointer(true),
|
||||
},
|
||||
}
|
||||
|
||||
result := worker.IsEnabled(config)
|
||||
|
||||
assert.Equal(t, result, true)
|
||||
})
|
||||
|
||||
t.Run("there is NO license then job is disabled", func(t *testing.T) {
|
||||
th := api4.SetupEnterpriseWithStoreMock(t)
|
||||
defer th.TearDown()
|
||||
|
||||
th.Server.SetLicense(nil)
|
||||
|
||||
osImpl := &OpensearchIndexerInterfaceImpl{
|
||||
Server: th.Server,
|
||||
}
|
||||
worker := osImpl.MakeWorker()
|
||||
|
||||
config := &model.Config{
|
||||
ElasticsearchSettings: model.ElasticsearchSettings{
|
||||
EnableIndexing: model.NewPointer(true),
|
||||
},
|
||||
}
|
||||
|
||||
result := worker.IsEnabled(config)
|
||||
|
||||
assert.Equal(t, result, false)
|
||||
})
|
||||
}
|
||||
|
||||
func TestOpenSearchIndexerPending(t *testing.T) {
|
||||
th := api4.SetupEnterprise(t).InitBasic()
|
||||
defer th.TearDown()
|
||||
|
||||
// Set up the state for the tests.
|
||||
th.App.UpdateConfig(func(cfg *model.Config) {
|
||||
*cfg.ElasticsearchSettings.EnableIndexing = true
|
||||
*cfg.ElasticsearchSettings.EnableSearching = true
|
||||
*cfg.ElasticsearchSettings.EnableAutocomplete = true
|
||||
*cfg.SqlSettings.DisableDatabaseSearch = true
|
||||
})
|
||||
th.App.Srv().SetLicense(model.NewTestLicense())
|
||||
|
||||
impl := OpensearchIndexerInterfaceImpl{
|
||||
Server: th.App.Srv(),
|
||||
}
|
||||
|
||||
worker := impl.MakeWorker()
|
||||
th.Server.Jobs.RegisterJobType(model.JobTypeElasticsearchPostIndexing, worker, nil)
|
||||
|
||||
go worker.Run()
|
||||
|
||||
job, appErr := th.App.Srv().Jobs.CreateJob(th.Context, model.JobTypeElasticsearchPostIndexing, map[string]string{})
|
||||
require.Nil(t, appErr)
|
||||
|
||||
worker.JobChannel() <- *job
|
||||
|
||||
worker.Stop()
|
||||
|
||||
job, err := th.App.Srv().Store().Job().Get(th.Context, job.Id)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, job.Status, model.JobStatusPending)
|
||||
}
|
||||
21
server/enterprise/elasticsearch/opensearch/main_test.go
Обычный файл
21
server/enterprise/elasticsearch/opensearch/main_test.go
Обычный файл
@@ -0,0 +1,21 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.enterprise for license information.
|
||||
|
||||
package opensearch
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/mattermost/mattermost/server/v8/channels/api4"
|
||||
"github.com/mattermost/mattermost/server/v8/channels/testlib"
|
||||
)
|
||||
|
||||
var mainHelper *testlib.MainHelper
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
mainHelper = testlib.NewMainHelper()
|
||||
defer mainHelper.Close()
|
||||
api4.SetMainHelper(mainHelper)
|
||||
|
||||
mainHelper.Main(m)
|
||||
}
|
||||
2104
server/enterprise/elasticsearch/opensearch/opensearch.go
Обычный файл
2104
server/enterprise/elasticsearch/opensearch/opensearch.go
Обычный файл
Разница между файлами не показана из-за своего большого размера
Загрузить разницу
126
server/enterprise/elasticsearch/opensearch/opensearch_test.go
Обычный файл
126
server/enterprise/elasticsearch/opensearch/opensearch_test.go
Обычный файл
@@ -0,0 +1,126 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.enterprise for license information.
|
||||
|
||||
package opensearch
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/opensearch-project/opensearch-go/v4/opensearchapi"
|
||||
"github.com/stretchr/testify/suite"
|
||||
|
||||
"github.com/mattermost/mattermost/server/public/model"
|
||||
"github.com/mattermost/mattermost/server/v8/channels/api4"
|
||||
"github.com/mattermost/mattermost/server/v8/enterprise/elasticsearch/common"
|
||||
"github.com/mattermost/mattermost/server/v8/platform/shared/filestore"
|
||||
"github.com/mattermost/mattermost/server/v8/platform/shared/filestore/mocks"
|
||||
)
|
||||
|
||||
type OpensearchInterfaceTestSuite struct {
|
||||
common.CommonTestSuite
|
||||
|
||||
th *api4.TestHelper
|
||||
client *opensearchapi.Client
|
||||
ctx context.Context
|
||||
fileBackend filestore.FileBackend
|
||||
}
|
||||
|
||||
func TestOpensearchInterfaceTestSuite(t *testing.T) {
|
||||
testSuite := &OpensearchInterfaceTestSuite{
|
||||
CommonTestSuite: common.CommonTestSuite{},
|
||||
}
|
||||
suite.Run(t, testSuite)
|
||||
}
|
||||
|
||||
func (s *OpensearchInterfaceTestSuite) SetupSuite() {
|
||||
if os.Getenv("IS_CI") == "true" {
|
||||
os.Setenv("MM_ELASTICSEARCHSETTINGS_CONNECTIONURL", "http://opensearch:9201")
|
||||
os.Setenv("MM_ELASTICSEARCHSETTINGS_BACKEND", "opensearch")
|
||||
}
|
||||
|
||||
s.th = api4.SetupEnterprise(s.T()).InitBasic()
|
||||
s.CommonTestSuite.TH = s.th
|
||||
s.CommonTestSuite.GetDocumentFn = func(index, documentID string) (bool, json.RawMessage, error) {
|
||||
resp, err := s.client.Document.Get(s.ctx, opensearchapi.DocumentGetReq{
|
||||
Index: index,
|
||||
DocumentID: documentID,
|
||||
})
|
||||
if resp == nil {
|
||||
return false, nil, err
|
||||
}
|
||||
return resp.Found, resp.Source, err
|
||||
}
|
||||
s.CommonTestSuite.RefreshIndexFn = func() error {
|
||||
_, err := s.client.Indices.Refresh(context.Background(), nil)
|
||||
return err
|
||||
}
|
||||
s.CommonTestSuite.CreateIndexFn = func(index string) error {
|
||||
_, err := s.client.Indices.Create(s.ctx, opensearchapi.IndicesCreateReq{
|
||||
Index: index,
|
||||
})
|
||||
return err
|
||||
}
|
||||
s.CommonTestSuite.GetIndexFn = func(indexPattern string) ([]string, error) {
|
||||
res, err := s.client.Indices.Get(s.ctx, opensearchapi.IndicesGetReq{
|
||||
Indices: []string{indexPattern},
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var names []string
|
||||
for name := range res.Indices {
|
||||
names = append(names, name)
|
||||
}
|
||||
return names, nil
|
||||
}
|
||||
|
||||
// Set up the state for the tests.
|
||||
s.th.App.UpdateConfig(func(cfg *model.Config) {
|
||||
if os.Getenv("IS_CI") == "true" {
|
||||
*cfg.ElasticsearchSettings.ConnectionURL = "http://opensearch:9201"
|
||||
} else {
|
||||
*cfg.ElasticsearchSettings.ConnectionURL = "http://localhost:9201"
|
||||
}
|
||||
*cfg.ElasticsearchSettings.Backend = model.ElasticsearchSettingsOSBackend
|
||||
*cfg.ElasticsearchSettings.EnableIndexing = true
|
||||
*cfg.ElasticsearchSettings.EnableSearching = true
|
||||
*cfg.ElasticsearchSettings.EnableAutocomplete = true
|
||||
*cfg.ElasticsearchSettings.LiveIndexingBatchSize = 1
|
||||
*cfg.SqlSettings.DisableDatabaseSearch = true
|
||||
})
|
||||
s.th.App.Srv().SetLicense(model.NewTestLicense())
|
||||
|
||||
if s.fileBackend == nil {
|
||||
s.fileBackend = &mocks.FileBackend{}
|
||||
}
|
||||
|
||||
// Initialise other stuff for the test.
|
||||
s.client = createTestClient(s.T(), s.th.Context, s.th.App.Config(), s.th.App.FileBackend())
|
||||
s.ctx = context.Background()
|
||||
|
||||
// Register search engine
|
||||
s.th.App.SearchEngine().RegisterElasticsearchEngine(&OpensearchInterfaceImpl{Platform: s.th.Server.Platform()})
|
||||
}
|
||||
|
||||
func (s *OpensearchInterfaceTestSuite) TearDownSuite() {
|
||||
if os.Getenv("IS_CI") == "true" {
|
||||
os.Setenv("MM_ELASTICSEARCHSETTINGS_CONNECTIONURL", "http://elasticsearch:9201")
|
||||
os.Unsetenv("MM_ELASTICSEARCHSETTINGS_BACKEND")
|
||||
}
|
||||
}
|
||||
|
||||
func (s *OpensearchInterfaceTestSuite) SetupTest() {
|
||||
s.CommonTestSuite.ESImpl = s.th.App.SearchEngine().ElasticsearchEngine
|
||||
|
||||
if s.CommonTestSuite.ESImpl.IsActive() {
|
||||
appErr := s.CommonTestSuite.ESImpl.Stop()
|
||||
s.Require().Nil(appErr)
|
||||
}
|
||||
|
||||
s.Require().Nil(s.CommonTestSuite.ESImpl.Start())
|
||||
|
||||
s.Nil(s.CommonTestSuite.ESImpl.PurgeIndexes(s.th.Context))
|
||||
}
|
||||
28
server/enterprise/elasticsearch/opensearch/testlib.go
Обычный файл
28
server/enterprise/elasticsearch/opensearch/testlib.go
Обычный файл
@@ -0,0 +1,28 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.enterprise for license information.
|
||||
|
||||
package opensearch
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/opensearch-project/opensearch-go/v4/opensearchapi"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/mattermost/mattermost/server/public/model"
|
||||
"github.com/mattermost/mattermost/server/public/shared/request"
|
||||
"github.com/mattermost/mattermost/server/v8/platform/shared/filestore"
|
||||
"github.com/mattermost/mattermost/server/v8/platform/shared/filestore/mocks"
|
||||
)
|
||||
|
||||
func createTestClient(t *testing.T, rctx request.CTX, cfg *model.Config, fileStore filestore.FileBackend) *opensearchapi.Client {
|
||||
t.Helper()
|
||||
|
||||
if fileStore == nil {
|
||||
fileStore = &mocks.FileBackend{}
|
||||
}
|
||||
|
||||
client, err := createClient(rctx.Logger(), cfg, fileStore, true)
|
||||
require.Nil(t, err)
|
||||
return client
|
||||
}
|
||||
Ссылка в новой задаче
Block a user