diff --git a/e2e-tests/cypress/tests/support/api/cloud_default_config.json b/e2e-tests/cypress/tests/support/api/cloud_default_config.json index 5d39dd1110d..6085054e1f1 100644 --- a/e2e-tests/cypress/tests/support/api/cloud_default_config.json +++ b/e2e-tests/cypress/tests/support/api/cloud_default_config.json @@ -292,6 +292,7 @@ "PublicCertificateFile": "", "PrivateKeyFile": "", "QueryTimeout": 60, + "AnalyticsQueryTimeout": 300, "MaxPageSize": 500, "LoginFieldName": "", "LoginButtonColor": "#0000", diff --git a/e2e-tests/cypress/tests/support/api/on_prem_default_config.json b/e2e-tests/cypress/tests/support/api/on_prem_default_config.json index 7fcc0482d49..a3d2d5f2a57 100644 --- a/e2e-tests/cypress/tests/support/api/on_prem_default_config.json +++ b/e2e-tests/cypress/tests/support/api/on_prem_default_config.json @@ -152,6 +152,7 @@ "Trace": false, "AtRestEncryptKey": "", "QueryTimeout": 30, + "AnalyticsQueryTimeout": 300, "DisableDatabaseSearch": false, "MigrationsStatementTimeoutSeconds": 100000, "ReplicaLagSettings": [], diff --git a/e2e-tests/playwright/lib/src/server/default_config.ts b/e2e-tests/playwright/lib/src/server/default_config.ts index 0f5982cf983..b893f626120 100644 --- a/e2e-tests/playwright/lib/src/server/default_config.ts +++ b/e2e-tests/playwright/lib/src/server/default_config.ts @@ -259,6 +259,7 @@ const defaultServerConfig: AdminConfig = { Trace: false, AtRestEncryptKey: '', QueryTimeout: 30, + AnalyticsQueryTimeout: 300, DisableDatabaseSearch: false, MigrationsStatementTimeoutSeconds: 100000, ReplicaLagSettings: [], diff --git a/server/channels/store/sqlstore/file_info_store.go b/server/channels/store/sqlstore/file_info_store.go index 753a6b38317..ea84b5b6847 100644 --- a/server/channels/store/sqlstore/file_info_store.go +++ b/server/channels/store/sqlstore/file_info_store.go @@ -794,12 +794,15 @@ func (fs SqlFileInfoStore) RestoreForPostByIds(rctx request.CTX, postId string, } func (fs SqlFileInfoStore) RefreshFileStats() error { + ctx, cancel := fs.analyticsContext() + defer cancel() + // CONCURRENTLY is not used deliberately because as per Postgres docs, // not using CONCURRENTLY takes less resources and completes faster // at the expense of locking the mat view. Since viewing admin console // is not a very frequent activity, we accept the tradeoff to let the // refresh happen as fast as possible. - if _, err := fs.GetMaster().Exec("REFRESH MATERIALIZED VIEW file_stats"); err != nil { + if _, err := fs.GetMaster().ExecContext(ctx, "REFRESH MATERIALIZED VIEW file_stats"); err != nil { return errors.Wrap(err, "error refreshing materialized view file_stats") } diff --git a/server/channels/store/sqlstore/post_store.go b/server/channels/store/sqlstore/post_store.go index 69c53c83181..e782f56be03 100644 --- a/server/channels/store/sqlstore/post_store.go +++ b/server/channels/store/sqlstore/post_store.go @@ -3267,11 +3267,18 @@ func (s *SqlPostStore) RefreshPostStats() error { // at the expense of locking the mat view. Since viewing admin console // is not a very frequent activity, we accept the tradeoff to let the // refresh happen as fast as possible. - if _, err := s.GetMaster().Exec("REFRESH MATERIALIZED VIEW posts_by_team_day"); err != nil { + + postsCtx, postsCancel := s.analyticsContext() + defer postsCancel() + + if _, err := s.GetMaster().ExecContext(postsCtx, "REFRESH MATERIALIZED VIEW posts_by_team_day"); err != nil { return errors.Wrap(err, "error refreshing materialized view posts_by_team_day") } - if _, err := s.GetMaster().Exec("REFRESH MATERIALIZED VIEW bot_posts_by_team_day"); err != nil { + botPostsCtx, botPostsCancel := s.analyticsContext() + defer botPostsCancel() + + if _, err := s.GetMaster().ExecContext(botPostsCtx, "REFRESH MATERIALIZED VIEW bot_posts_by_team_day"); err != nil { return errors.Wrap(err, "error refreshing materialized view bot_posts_by_team_day") } diff --git a/server/channels/store/sqlstore/store.go b/server/channels/store/sqlstore/store.go index a806e8d956f..f63b76ec6be 100644 --- a/server/channels/store/sqlstore/store.go +++ b/server/channels/store/sqlstore/store.go @@ -4,6 +4,7 @@ package sqlstore import ( + "context" "database/sql" "fmt" "path" @@ -474,6 +475,10 @@ func (ss *SqlStore) GetReplica() *sqlxDBWrapper { return ss.GetMaster() } +func (ss *SqlStore) analyticsContext() (context.Context, context.CancelFunc) { + return context.WithTimeout(context.Background(), time.Duration(*ss.settings.AnalyticsQueryTimeout)*time.Second) +} + func (ss *SqlStore) monitorReplicas() { t := time.NewTicker(time.Duration(*ss.settings.ReplicaMonitorIntervalSeconds) * time.Second) defer func() { diff --git a/server/channels/store/sqlstore/user_store.go b/server/channels/store/sqlstore/user_store.go index 82f535db130..523355432d7 100644 --- a/server/channels/store/sqlstore/user_store.go +++ b/server/channels/store/sqlstore/user_store.go @@ -2374,9 +2374,13 @@ func (us SqlUserStore) GetUsersWithInvalidEmails(page int, perPage int, restrict } func (us SqlUserStore) RefreshPostStatsForUsers() error { - if _, err := us.GetMaster().Exec("REFRESH MATERIALIZED VIEW poststats"); err != nil { + ctx, cancel := us.analyticsContext() + defer cancel() + + if _, err := us.GetMaster().ExecContext(ctx, "REFRESH MATERIALIZED VIEW poststats"); err != nil { return errors.Wrap(err, "users_refresh_post_stats_exec") } + return nil } diff --git a/server/channels/store/storetest/settings.go b/server/channels/store/storetest/settings.go index 900ab444dd5..92e59c97687 100644 --- a/server/channels/store/storetest/settings.go +++ b/server/channels/store/storetest/settings.go @@ -90,6 +90,7 @@ func databaseSettings(driver, dataSource string) *model.SqlSettings { Trace: model.NewPointer(false), AtRestEncryptKey: model.NewPointer(model.NewRandomString(32)), QueryTimeout: new(int), + AnalyticsQueryTimeout: new(int), MigrationsStatementTimeoutSeconds: new(int), } *settings.MaxIdleConns = 10 @@ -97,6 +98,7 @@ func databaseSettings(driver, dataSource string) *model.SqlSettings { *settings.ConnMaxIdleTimeMilliseconds = 300000 *settings.MaxOpenConns = 100 *settings.QueryTimeout = 60 + *settings.AnalyticsQueryTimeout = 300 *settings.MigrationsStatementTimeoutSeconds = 60 return settings diff --git a/server/i18n/en.json b/server/i18n/en.json index 848966cfb60..707536fadcd 100644 --- a/server/i18n/en.json +++ b/server/i18n/en.json @@ -11028,6 +11028,10 @@ "id": "model.config.is_valid.sitename_length.app_error", "translation": "Site name must be less than or equal to {{.MaxLength}} characters." }, + { + "id": "model.config.is_valid.sql_analytics_query_timeout.app_error", + "translation": "Invalid analytics query timeout for SQL settings. Must be a positive number." + }, { "id": "model.config.is_valid.sql_conn_max_idle_time_milliseconds.app_error", "translation": "Invalid connection maximum idle time for SQL settings. Must be a non-negative number." diff --git a/server/public/model/config.go b/server/public/model/config.go index 338be363b77..3ec6d8253ca 100644 --- a/server/public/model/config.go +++ b/server/public/model/config.go @@ -1491,6 +1491,7 @@ type SqlSettings struct { Trace *bool `access:"environment_database,write_restrictable,cloud_restrictable"` AtRestEncryptKey *string `access:"environment_database,write_restrictable,cloud_restrictable"` // telemetry: none QueryTimeout *int `access:"environment_database,write_restrictable,cloud_restrictable"` + AnalyticsQueryTimeout *int `access:"environment_database,write_restrictable,cloud_restrictable"` DisableDatabaseSearch *bool `access:"environment_database,write_restrictable,cloud_restrictable"` MigrationsStatementTimeoutSeconds *int `access:"environment_database,write_restrictable,cloud_restrictable"` ReplicaLagSettings []*ReplicaLagSettings `access:"environment_database,write_restrictable,cloud_restrictable"` // telemetry: none @@ -1548,6 +1549,10 @@ func (s *SqlSettings) SetDefaults(isUpdate bool) { s.QueryTimeout = NewPointer(30) } + if s.AnalyticsQueryTimeout == nil { + s.AnalyticsQueryTimeout = NewPointer(300) + } + if s.DisableDatabaseSearch == nil { s.DisableDatabaseSearch = NewPointer(false) } @@ -4366,6 +4371,10 @@ func (s *SqlSettings) isValid() *AppError { return NewAppError("Config.IsValid", "model.config.is_valid.sql_query_timeout.app_error", nil, "", http.StatusBadRequest) } + if *s.AnalyticsQueryTimeout <= 0 { + return NewAppError("Config.IsValid", "model.config.is_valid.sql_analytics_query_timeout.app_error", nil, "", http.StatusBadRequest) + } + if *s.DataSource == "" { return NewAppError("Config.IsValid", "model.config.is_valid.sql_data_src.app_error", nil, "", http.StatusBadRequest) } diff --git a/webapp/channels/src/components/admin_console/__snapshots__/database_settings.test.tsx.snap b/webapp/channels/src/components/admin_console/__snapshots__/database_settings.test.tsx.snap index 08a37e8882e..ab2589c7f54 100644 --- a/webapp/channels/src/components/admin_console/__snapshots__/database_settings.test.tsx.snap +++ b/webapp/channels/src/components/admin_console/__snapshots__/database_settings.test.tsx.snap @@ -168,6 +168,37 @@ exports[`components/DatabaseSettings should match snapshot 1`] = ` +