mirror of
https://github.com/mattermost/mattermost.git
synced 2026-08-26 21:27:40 -05:00
MM-68248: Handle missing indexes gracefully before reindex
OpenSearch v3 rejects _update_by_query and _delete_by_query with no index argument (405), and returns index_not_found_exception (404) when querying an exact index name that hasn't been created yet. Both arise before any reindex has run, since indexes are created on first document write. Return nil/empty instead of an error from all affected operations, and add test coverage for each in the no-indexes state.
This commit is contained in:
@@ -7,6 +7,7 @@ import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"slices"
|
||||
@@ -40,6 +41,14 @@ var (
|
||||
purgeIndexListAllowedIndexes = []string{common.IndexBaseChannels}
|
||||
)
|
||||
|
||||
// isIndexNotFound reports whether err is a 404 index_not_found_exception from
|
||||
// OpenSearch. This happens when an index has never been created (e.g. no
|
||||
// reindex has run yet) and should be treated as an empty result, not an error.
|
||||
func isIndexNotFound(err error) bool {
|
||||
var osErr *opensearch.StructError
|
||||
return errors.As(err, &osErr) && osErr.Status == http.StatusNotFound
|
||||
}
|
||||
|
||||
type OpensearchInterfaceImpl struct {
|
||||
client *opensearchapi.Client
|
||||
mutex sync.RWMutex
|
||||
@@ -840,6 +849,9 @@ func (os *OpensearchInterfaceImpl) DeleteChannelPosts(rctx request.CTX, channelI
|
||||
if err != nil {
|
||||
return model.NewAppError("Opensearch.DeleteChannelPosts", "ent.elasticsearch.delete_channel_posts.error", nil, "", http.StatusInternalServerError).Wrap(err)
|
||||
}
|
||||
if len(postIndexes) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Duration(*os.Platform.Config().ElasticsearchSettings.RequestTimeoutSeconds)*time.Second)
|
||||
defer cancel()
|
||||
@@ -881,6 +893,9 @@ func (os *OpensearchInterfaceImpl) UpdatePostsChannelTypeByChannelId(rctx reques
|
||||
if err != nil {
|
||||
return model.NewAppError("Opensearch.UpdatePostsChannelTypeByChannelId", "ent.elasticsearch.update_posts_channel_type.error", map[string]any{"Backend": model.ElasticsearchSettingsOSBackend}, "", http.StatusInternalServerError).Wrap(err)
|
||||
}
|
||||
if len(postIndexes) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Duration(*os.Platform.Config().ElasticsearchSettings.RequestTimeoutSeconds)*time.Second)
|
||||
defer cancel()
|
||||
@@ -943,6 +958,9 @@ func (os *OpensearchInterfaceImpl) BackfillPostsChannelType(rctx request.CTX, ch
|
||||
if err != nil {
|
||||
return model.NewAppError("Opensearch.BackfillPostsChannelType", "ent.elasticsearch.backfill_posts_channel_type.error", map[string]any{"Backend": model.ElasticsearchSettingsOSBackend}, "", http.StatusInternalServerError).Wrap(err)
|
||||
}
|
||||
if len(postIndexes) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Hour)
|
||||
defer cancel()
|
||||
@@ -1014,6 +1032,9 @@ func (os *OpensearchInterfaceImpl) DeleteUserPosts(rctx request.CTX, userID stri
|
||||
if err != nil {
|
||||
return model.NewAppError("Opensearch.DeleteUserPosts", "ent.elasticsearch.delete_user_posts.error", nil, "", http.StatusInternalServerError).Wrap(err)
|
||||
}
|
||||
if len(postIndexes) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Duration(*os.Platform.Config().ElasticsearchSettings.RequestTimeoutSeconds)*time.Second)
|
||||
defer cancel()
|
||||
@@ -2127,6 +2148,9 @@ func (os *OpensearchInterfaceImpl) SearchFiles(channels model.ChannelList, searc
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
if isIndexNotFound(err) {
|
||||
return []string{}, nil
|
||||
}
|
||||
errorStr := "err=" + err.Error()
|
||||
if *os.Platform.Config().ElasticsearchSettings.Trace == "error" {
|
||||
errorStr = "Query=" + getJSONOrErrorStr(query) + ", " + errorStr
|
||||
@@ -2210,6 +2234,9 @@ func (os *OpensearchInterfaceImpl) DeleteUserFiles(rctx request.CTX, userID stri
|
||||
Body: bytes.NewReader(queryBuf),
|
||||
})
|
||||
if err != nil {
|
||||
if isIndexNotFound(err) {
|
||||
return nil
|
||||
}
|
||||
return model.NewAppError("Opensearch.DeleteUserFiles", "ent.elasticsearch.delete_user_files.error", nil, "", http.StatusInternalServerError).Wrap(err)
|
||||
}
|
||||
rctx.Logger().Info("User files deleted", mlog.String("user_id", userID), mlog.Int("deleted", response.Deleted))
|
||||
@@ -2246,6 +2273,9 @@ func (os *OpensearchInterfaceImpl) DeletePostFiles(rctx request.CTX, postID stri
|
||||
Body: bytes.NewReader(queryBuf),
|
||||
})
|
||||
if err != nil {
|
||||
if isIndexNotFound(err) {
|
||||
return nil
|
||||
}
|
||||
return model.NewAppError("Opensearch.DeletePostFiles", "ent.elasticsearch.delete_post_files.error", nil, "", http.StatusInternalServerError).Wrap(err)
|
||||
}
|
||||
rctx.Logger().Info("Post files deleted", mlog.String("post_id", postID), mlog.Int("deleted", response.Deleted))
|
||||
@@ -2293,6 +2323,9 @@ func (os *OpensearchInterfaceImpl) DeleteFilesBatch(rctx request.CTX, endTime, l
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
if isIndexNotFound(err) {
|
||||
return nil
|
||||
}
|
||||
return model.NewAppError("Opensearch.DeleteUserPosts", "ent.elasticsearch.delete_user_posts.error", nil, "", http.StatusInternalServerError).Wrap(err)
|
||||
}
|
||||
rctx.Logger().Info("Files batch deleted", mlog.Int("end_time", endTime), mlog.Int("limit", limit), mlog.Int("deleted", response.Deleted))
|
||||
|
||||
@@ -212,6 +212,59 @@ func (s *OpensearchInterfaceTestSuite) TestSyncBulkIndexChannels() {
|
||||
})
|
||||
}
|
||||
|
||||
// TestNoIndexesGracefulHandling verifies that write and search operations
|
||||
// return nil/empty (not an error) when no indexes exist yet. This covers the
|
||||
// state before any reindex has run: the index templates are present but the
|
||||
// actual indexes have never been created.
|
||||
func (s *OpensearchInterfaceTestSuite) TestNoIndexesGracefulHandling() {
|
||||
// SetupTest already calls PurgeIndexes, so there are no indexes at this point.
|
||||
impl := s.CommonTestSuite.ESImpl
|
||||
rctx := s.th.Context
|
||||
|
||||
s.Run("BackfillPostsChannelType", func() {
|
||||
appErr := impl.BackfillPostsChannelType(rctx, []string{"channel1", "channel2"}, "O")
|
||||
s.Nil(appErr)
|
||||
})
|
||||
|
||||
s.Run("DeleteChannelPosts", func() {
|
||||
appErr := impl.DeleteChannelPosts(rctx, s.th.BasicChannel.Id)
|
||||
s.Nil(appErr)
|
||||
})
|
||||
|
||||
s.Run("DeleteUserPosts", func() {
|
||||
appErr := impl.DeleteUserPosts(rctx, s.th.BasicUser.Id)
|
||||
s.Nil(appErr)
|
||||
})
|
||||
|
||||
s.Run("UpdatePostsChannelTypeByChannelId", func() {
|
||||
appErr := impl.UpdatePostsChannelTypeByChannelId(rctx, s.th.BasicChannel.Id, "O")
|
||||
s.Nil(appErr)
|
||||
})
|
||||
|
||||
s.Run("SearchFiles", func() {
|
||||
channels := model.ChannelList{s.th.BasicChannel}
|
||||
params := model.ParseSearchParams("test", 0)
|
||||
fileIDs, appErr := impl.SearchFiles(channels, params, 0, 20)
|
||||
s.Nil(appErr)
|
||||
s.Empty(fileIDs)
|
||||
})
|
||||
|
||||
s.Run("DeletePostFiles", func() {
|
||||
appErr := impl.DeletePostFiles(rctx, s.th.BasicPost.Id)
|
||||
s.Nil(appErr)
|
||||
})
|
||||
|
||||
s.Run("DeleteUserFiles", func() {
|
||||
appErr := impl.DeleteUserFiles(rctx, s.th.BasicUser.Id)
|
||||
s.Nil(appErr)
|
||||
})
|
||||
|
||||
s.Run("DeleteFilesBatch", func() {
|
||||
appErr := impl.DeleteFilesBatch(rctx, model.GetMillis(), 1000)
|
||||
s.Nil(appErr)
|
||||
})
|
||||
}
|
||||
|
||||
func (s *OpensearchInterfaceTestSuite) TestTemplateCreationClientError() {
|
||||
s.Run("Should handle error with CausedBy information from opensearch", func() {
|
||||
// Invalid template request that will trigger an error with caused_by
|
||||
|
||||
Reference in New Issue
Block a user