From 7c1b8cd937f54866a6d0bad5f2e62b88dbe5b028 Mon Sep 17 00:00:00 2001 From: Agniva De Sarker Date: Sat, 11 Jun 2022 00:24:46 +0530 Subject: [PATCH] MM-43733: Set concurrency limits via configuration (#20413) ```release-note We create a new config option MaxImageDecoderConcurrency which indicates how many images can be decoded concurrently at once. The default is -1 which means number of CPUs present. This affects the total memory consumption of the server. The maximum memory of a single image is dictated by MaxImageResolution * 24 bytes. Therefore, a good rule of thumb to follow is that MaxImageResolution * MaxImageDecoderConcurrency * 24 should be less then the allocated memory for image decoding. ``` https://mattermost.atlassian.net/browse/MM-43733 --- app/channels.go | 6 +++- i18n/en.json | 4 +++ model/config.go | 53 +++++++++++++++++++-------------- services/telemetry/telemetry.go | 31 +++++++++---------- 4 files changed, 56 insertions(+), 38 deletions(-) diff --git a/app/channels.go b/app/channels.go index 07692fe95d..cd9aa3d7dd 100644 --- a/app/channels.go +++ b/app/channels.go @@ -197,8 +197,12 @@ func NewChannels(s *Server, services map[ServiceKey]interface{}) (*Channels, err } var imgErr error + decoderConcurrency := int(*ch.cfgSvc.Config().FileSettings.MaxImageDecoderConcurrency) + if decoderConcurrency == -1 { + decoderConcurrency = runtime.NumCPU() + } ch.imgDecoder, imgErr = imaging.NewDecoder(imaging.DecoderOptions{ - ConcurrencyLevel: runtime.NumCPU(), + ConcurrencyLevel: decoderConcurrency, }) if imgErr != nil { return nil, errors.Wrap(imgErr, "failed to create image decoder") diff --git a/i18n/en.json b/i18n/en.json index cfaea23aea..30b842f1f5 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -8095,6 +8095,10 @@ "id": "model.config.is_valid.group_unread_channels.app_error", "translation": "Invalid group unread channels for service settings. Must be 'disabled', 'default_on', or 'default_off'." }, + { + "id": "model.config.is_valid.image_decoder_concurrency.app_error", + "translation": "Invalid decoder concurrency {{.Value}}. Should be a positive number or -1." + }, { "id": "model.config.is_valid.image_proxy_type.app_error", "translation": "Invalid image proxy type. Must be 'local' or 'atmos/camo'." diff --git a/model/config.go b/model/config.go index 741804acec..7c29fc5ac0 100644 --- a/model/config.go +++ b/model/config.go @@ -1401,28 +1401,29 @@ func (s *PasswordSettings) SetDefaults() { } type FileSettings struct { - EnableFileAttachments *bool `access:"site_file_sharing_and_downloads,cloud_restrictable"` - EnableMobileUpload *bool `access:"site_file_sharing_and_downloads,cloud_restrictable"` - EnableMobileDownload *bool `access:"site_file_sharing_and_downloads,cloud_restrictable"` - MaxFileSize *int64 `access:"environment_file_storage,cloud_restrictable"` - MaxImageResolution *int64 `access:"environment_file_storage,cloud_restrictable"` - DriverName *string `access:"environment_file_storage,write_restrictable,cloud_restrictable"` - Directory *string `access:"environment_file_storage,write_restrictable,cloud_restrictable"` - EnablePublicLink *bool `access:"site_public_links,cloud_restrictable"` - ExtractContent *bool `access:"environment_file_storage,write_restrictable"` - ArchiveRecursion *bool `access:"environment_file_storage,write_restrictable"` - PublicLinkSalt *string `access:"site_public_links,cloud_restrictable"` // telemetry: none - InitialFont *string `access:"environment_file_storage,cloud_restrictable"` // telemetry: none - AmazonS3AccessKeyId *string `access:"environment_file_storage,write_restrictable,cloud_restrictable"` // telemetry: none - AmazonS3SecretAccessKey *string `access:"environment_file_storage,write_restrictable,cloud_restrictable"` // telemetry: none - AmazonS3Bucket *string `access:"environment_file_storage,write_restrictable,cloud_restrictable"` // telemetry: none - AmazonS3PathPrefix *string `access:"environment_file_storage,write_restrictable,cloud_restrictable"` // telemetry: none - AmazonS3Region *string `access:"environment_file_storage,write_restrictable,cloud_restrictable"` // telemetry: none - AmazonS3Endpoint *string `access:"environment_file_storage,write_restrictable,cloud_restrictable"` // telemetry: none - AmazonS3SSL *bool `access:"environment_file_storage,write_restrictable,cloud_restrictable"` - AmazonS3SignV2 *bool `access:"environment_file_storage,write_restrictable,cloud_restrictable"` - AmazonS3SSE *bool `access:"environment_file_storage,write_restrictable,cloud_restrictable"` - AmazonS3Trace *bool `access:"environment_file_storage,write_restrictable,cloud_restrictable"` + EnableFileAttachments *bool `access:"site_file_sharing_and_downloads,cloud_restrictable"` + EnableMobileUpload *bool `access:"site_file_sharing_and_downloads,cloud_restrictable"` + EnableMobileDownload *bool `access:"site_file_sharing_and_downloads,cloud_restrictable"` + MaxFileSize *int64 `access:"environment_file_storage,cloud_restrictable"` + MaxImageResolution *int64 `access:"environment_file_storage,cloud_restrictable"` + MaxImageDecoderConcurrency *int64 `access:"environment_file_storage,cloud_restrictable"` + DriverName *string `access:"environment_file_storage,write_restrictable,cloud_restrictable"` + Directory *string `access:"environment_file_storage,write_restrictable,cloud_restrictable"` + EnablePublicLink *bool `access:"site_public_links,cloud_restrictable"` + ExtractContent *bool `access:"environment_file_storage,write_restrictable"` + ArchiveRecursion *bool `access:"environment_file_storage,write_restrictable"` + PublicLinkSalt *string `access:"site_public_links,cloud_restrictable"` // telemetry: none + InitialFont *string `access:"environment_file_storage,cloud_restrictable"` // telemetry: none + AmazonS3AccessKeyId *string `access:"environment_file_storage,write_restrictable,cloud_restrictable"` // telemetry: none + AmazonS3SecretAccessKey *string `access:"environment_file_storage,write_restrictable,cloud_restrictable"` // telemetry: none + AmazonS3Bucket *string `access:"environment_file_storage,write_restrictable,cloud_restrictable"` // telemetry: none + AmazonS3PathPrefix *string `access:"environment_file_storage,write_restrictable,cloud_restrictable"` // telemetry: none + AmazonS3Region *string `access:"environment_file_storage,write_restrictable,cloud_restrictable"` // telemetry: none + AmazonS3Endpoint *string `access:"environment_file_storage,write_restrictable,cloud_restrictable"` // telemetry: none + AmazonS3SSL *bool `access:"environment_file_storage,write_restrictable,cloud_restrictable"` + AmazonS3SignV2 *bool `access:"environment_file_storage,write_restrictable,cloud_restrictable"` + AmazonS3SSE *bool `access:"environment_file_storage,write_restrictable,cloud_restrictable"` + AmazonS3Trace *bool `access:"environment_file_storage,write_restrictable,cloud_restrictable"` } func (s *FileSettings) SetDefaults(isUpdate bool) { @@ -1446,6 +1447,10 @@ func (s *FileSettings) SetDefaults(isUpdate bool) { s.MaxImageResolution = NewInt64(7680 * 4320) // 8K, ~33MPX } + if s.MaxImageDecoderConcurrency == nil { + s.MaxImageDecoderConcurrency = NewInt64(-1) // Default to NumCPU + } + if s.DriverName == nil { s.DriverName = NewString(ImageDriverLocal) } @@ -3395,6 +3400,10 @@ func (s *FileSettings) isValid() *AppError { return NewAppError("Config.IsValid", "model.config.is_valid.directory.app_error", nil, "", http.StatusBadRequest) } + if *s.MaxImageDecoderConcurrency < -1 || *s.MaxImageDecoderConcurrency == 0 { + return NewAppError("Config.IsValid", "model.config.is_valid.image_decoder_concurrency.app_error", map[string]interface{}{"Value": *s.MaxImageDecoderConcurrency}, "", http.StatusBadRequest) + } + return nil } diff --git a/services/telemetry/telemetry.go b/services/telemetry/telemetry.go index aaeb3c4e47..7088abf004 100644 --- a/services/telemetry/telemetry.go +++ b/services/telemetry/telemetry.go @@ -525,21 +525,22 @@ func (ts *TelemetryService) trackConfig() { }) ts.SendTelemetry(TrackConfigFile, map[string]interface{}{ - "enable_public_links": cfg.FileSettings.EnablePublicLink, - "driver_name": *cfg.FileSettings.DriverName, - "isdefault_directory": isDefault(*cfg.FileSettings.Directory, model.FileSettingsDefaultDirectory), - "isabsolute_directory": filepath.IsAbs(*cfg.FileSettings.Directory), - "extract_content": *cfg.FileSettings.ExtractContent, - "archive_recursion": *cfg.FileSettings.ArchiveRecursion, - "amazon_s3_ssl": *cfg.FileSettings.AmazonS3SSL, - "amazon_s3_sse": *cfg.FileSettings.AmazonS3SSE, - "amazon_s3_signv2": *cfg.FileSettings.AmazonS3SignV2, - "amazon_s3_trace": *cfg.FileSettings.AmazonS3Trace, - "max_file_size": *cfg.FileSettings.MaxFileSize, - "max_image_resolution": *cfg.FileSettings.MaxImageResolution, - "enable_file_attachments": *cfg.FileSettings.EnableFileAttachments, - "enable_mobile_upload": *cfg.FileSettings.EnableMobileUpload, - "enable_mobile_download": *cfg.FileSettings.EnableMobileDownload, + "enable_public_links": cfg.FileSettings.EnablePublicLink, + "driver_name": *cfg.FileSettings.DriverName, + "isdefault_directory": isDefault(*cfg.FileSettings.Directory, model.FileSettingsDefaultDirectory), + "isabsolute_directory": filepath.IsAbs(*cfg.FileSettings.Directory), + "extract_content": *cfg.FileSettings.ExtractContent, + "archive_recursion": *cfg.FileSettings.ArchiveRecursion, + "amazon_s3_ssl": *cfg.FileSettings.AmazonS3SSL, + "amazon_s3_sse": *cfg.FileSettings.AmazonS3SSE, + "amazon_s3_signv2": *cfg.FileSettings.AmazonS3SignV2, + "amazon_s3_trace": *cfg.FileSettings.AmazonS3Trace, + "max_file_size": *cfg.FileSettings.MaxFileSize, + "max_image_resolution": *cfg.FileSettings.MaxImageResolution, + "max_image_decoder_concurrency": *cfg.FileSettings.MaxImageDecoderConcurrency, + "enable_file_attachments": *cfg.FileSettings.EnableFileAttachments, + "enable_mobile_upload": *cfg.FileSettings.EnableMobileUpload, + "enable_mobile_download": *cfg.FileSettings.EnableMobileDownload, }) ts.SendTelemetry(TrackConfigEmail, map[string]interface{}{