From 5d30f33577e69befc6e6a993eaa8fe146ce4add2 Mon Sep 17 00:00:00 2001 From: Carson Long Date: Mon, 6 Jun 2022 18:13:21 +0000 Subject: [PATCH] Adds log rate limit fields in bytes per second Updates the `/v3/organization_quotas/` and `/v3/space_quotas/` endpoints to allow setting and retrieving of a new parameter (`log_rate_limit_in_bytes_per_second`). This will eventually permit the user to set log line production limits in bytes per second, rather than lines per second. Updates v3/processes and v3/tasks endpoints to support `log_rate_limit_in_bytes_per_second` An unlimited log rate limit is represented as -1 Tracker Story ID: [#182311424] Tracker Story ID: [#182353823] Tracker Story ID: [#182311433] Tracker Story ID: [#182624538] Tracker Story ID: [#182624530] Github Issue: https://github.com/cloudfoundry/capi-release/issues/245 Signed-off-by: Carson Long Signed-off-by: Kenneth Lakin Signed-off-by: Duane May Signed-off-by: Matthew Kocher Signed-off-by: Ben Fuller Signed-off-by: Seth Boyles Co-authored-by: Matthew Kocher --- app/actions/organization_quotas_create.rb | 1 + app/actions/organization_quotas_update.rb | 5 + app/actions/process_scale.rb | 1 + app/actions/space_diff_manifest.rb | 25 ++- app/actions/space_quota_update.rb | 5 + app/actions/space_quotas_create.rb | 3 + app/actions/task_create.rb | 11 +- app/controllers/runtime/apps_controller.rb | 1 + app/controllers/v3/processes_controller.rb | 1 + app/messages/app_manifest_message.rb | 22 ++- app/messages/base_message.rb | 1 + .../manifest_process_scale_message.rb | 6 +- .../organization_quotas_update_message.rb | 2 +- app/messages/process_scale_message.rb | 3 +- app/messages/quotas_apps_message.rb | 6 +- app/messages/space_quota_update_message.rb | 2 +- app/messages/task_create_message.rb | 3 +- app/models.rb | 2 + .../constraints/max_log_rate_limit_policy.rb | 78 +++++++++ .../constraints/min_log_rate_limit_policy.rb | 16 ++ app/models/runtime/organization.rb | 22 ++- app/models/runtime/process_model.rb | 22 ++- app/models/runtime/quota_definition.rb | 25 +-- app/models/runtime/space.rb | 26 +++ app/models/runtime/space_quota_definition.rb | 20 ++- app/models/runtime/task_model.rb | 4 + app/presenters/v2/process_model_presenter.rb | 1 + .../process_properties_presenter.rb | 13 ++ .../v3/organization_quota_presenter.rb | 1 + app/presenters/v3/process_presenter.rb | 17 +- app/presenters/v3/process_stats_presenter.rb | 2 + app/presenters/v3/space_quota_presenter.rb | 1 + app/presenters/v3/task_presenter.rb | 1 + config/bosh-lite.yml | 1 + config/cloud_controller.yml | 1 + ...add_log_rate_limit_to_quota_definitions.rb | 5 + ...g_rate_limit_to_space_quota_definitions.rb | 5 + ...8165055_add_log_rate_limit_to_processes.rb | 5 + ...75820_add_log_rate_limit_to_tasks_table.rb | 5 + .../app_manifest/byte_converter.rb | 30 ++++ .../process_log_rate_limit_calculator.rb | 51 ++++++ .../config_schemas/base/api_schema.rb | 1 + .../config_schemas/base/clock_schema.rb | 1 + .../config_schemas/base/worker_schema.rb | 1 + spec/request/app_manifests_spec.rb | 6 + spec/request/apps_spec.rb | 146 ++++++++++++++++ spec/request/organization_quotas_spec.rb | 23 ++- spec/request/processes_spec.rb | 115 +++++++++++-- spec/request/space_manifests_spec.rb | 57 ++++++ spec/request/space_quotas_spec.rb | 23 ++- spec/request/tasks_spec.rb | 124 ++++++++++++-- spec/request/v2/apps_spec.rb | 11 ++ spec/request/v2/routes_spec.rb | 2 + spec/request/v2/service_bindings_spec.rb | 1 + spec/request/v2/spaces_spec.rb | 1 + .../organization_quotas_create_spec.rb | 5 +- .../organization_quotas_update_spec.rb | 5 +- spec/unit/actions/process_scale_spec.rb | 30 +++- spec/unit/actions/space_diff_manifest_spec.rb | 8 +- spec/unit/actions/space_quota_update_spec.rb | 5 +- spec/unit/actions/space_quotas_create_spec.rb | 3 + spec/unit/actions/task_create_spec.rb | 51 +++++- .../runtime/apps_controller_spec.rb | 2 + .../app_manifest/byte_converter_spec.rb | 72 ++++++++ .../process_log_rate_limit_calculator_spec.rb | 132 ++++++++++++++ .../messages/app_manifest_message_spec.rb | 67 +++++++- .../manifest_process_scale_message_spec.rb | 51 +++++- ...organization_quotas_create_message_spec.rb | 68 ++++++++ .../messages/process_scale_message_spec.rb | 48 ++++++ .../unit/messages/quotas_apps_message_spec.rb | 62 +++++++ .../space_quota_update_message_spec.rb | 2 + .../unit/messages/task_create_message_spec.rb | 53 ++++++ .../max_log_rate_limit_policy_spec.rb | 162 ++++++++++++++++++ .../min_log_rate_limit_policy_spec.rb | 27 +++ spec/unit/models/runtime/organization_spec.rb | 53 ++++++ .../unit/models/runtime/process_model_spec.rb | 88 +++++++++- .../models/runtime/quota_definition_spec.rb | 15 +- .../runtime/space_quota_definition_spec.rb | 15 +- spec/unit/models/runtime/space_spec.rb | 51 ++++++ spec/unit/models/runtime/task_model_spec.rb | 85 +++++++++ .../v2/process_model_presenter_spec.rb | 1 + .../v3/app_manifest_presenter_spec.rb | 5 + .../process_properties_presenter_spec.rb | 26 +++ .../presenters/v3/process_presenter_spec.rb | 9 + .../v3/process_stats_presenter_spec.rb | 16 +- .../v3/space_quota_presenter_spec.rb | 4 + .../unit/presenters/v3/task_presenter_spec.rb | 2 + 87 files changed, 2066 insertions(+), 121 deletions(-) create mode 100644 app/models/runtime/constraints/max_log_rate_limit_policy.rb create mode 100644 app/models/runtime/constraints/min_log_rate_limit_policy.rb create mode 100644 db/migrations/20220606172913_add_log_rate_limit_to_quota_definitions.rb create mode 100644 db/migrations/20220607222309_add_log_rate_limit_to_space_quota_definitions.rb create mode 100644 db/migrations/20220608165055_add_log_rate_limit_to_processes.rb create mode 100644 db/migrations/20220621175820_add_log_rate_limit_to_tasks_table.rb create mode 100644 lib/cloud_controller/app_services/process_log_rate_limit_calculator.rb create mode 100644 spec/unit/lib/cloud_controller/app_services/process_log_rate_limit_calculator_spec.rb create mode 100644 spec/unit/models/runtime/constraints/max_log_rate_limit_policy_spec.rb create mode 100644 spec/unit/models/runtime/constraints/min_log_rate_limit_policy_spec.rb diff --git a/app/actions/organization_quotas_create.rb b/app/actions/organization_quotas_create.rb index 07e0096dc04..cf3f7e85815 100644 --- a/app/actions/organization_quotas_create.rb +++ b/app/actions/organization_quotas_create.rb @@ -16,6 +16,7 @@ def create(message) instance_memory_limit: message.per_process_memory_in_mb || QuotaDefinition::UNLIMITED, app_instance_limit: message.total_instances || QuotaDefinition::UNLIMITED, app_task_limit: message.per_app_tasks || QuotaDefinition::UNLIMITED, + log_rate_limit: message.log_rate_limit_in_bytes_per_second || QuotaDefinition::UNLIMITED, # Services total_services: message.total_service_instances || QuotaDefinition::DEFAULT_TOTAL_SERVICES, diff --git a/app/actions/organization_quotas_update.rb b/app/actions/organization_quotas_update.rb index 0cf74c7d98f..12f7cfcd35c 100644 --- a/app/actions/organization_quotas_update.rb +++ b/app/actions/organization_quotas_update.rb @@ -13,6 +13,7 @@ def self.update(quota, message) quota.instance_memory_limit = instance_memory_limit(message) if message.apps_limits_message.requested? :per_process_memory_in_mb quota.app_instance_limit = app_instance_limit(message) if message.apps_limits_message.requested? :total_instances quota.app_task_limit = app_task_limit(message) if message.apps_limits_message.requested? :per_app_tasks + quota.log_rate_limit = log_rate_limit(message) if message.apps_limits_message.requested? :log_rate_limit_in_bytes_per_second quota.total_services = total_services(message) if message.services_limits_message.requested? :total_service_instances quota.total_service_keys = total_service_keys(message) if message.services_limits_message.requested? :total_service_keys @@ -56,6 +57,10 @@ def self.app_task_limit(message) default_if_nil(message.per_app_tasks, QuotaDefinition::UNLIMITED) end + def self.log_rate_limit(message) + default_if_nil(message.log_rate_limit_in_bytes_per_second, QuotaDefinition::UNLIMITED) + end + def self.total_services(message) default_if_nil(message.total_service_instances, QuotaDefinition::UNLIMITED) end diff --git a/app/actions/process_scale.rb b/app/actions/process_scale.rb index d13362fb9d9..2f6763a516b 100644 --- a/app/actions/process_scale.rb +++ b/app/actions/process_scale.rb @@ -22,6 +22,7 @@ def scale @process.instances = @message.instances if @message.requested?(:instances) @process.memory = @message.memory_in_mb if @message.requested?(:memory_in_mb) @process.disk_quota = @message.disk_in_mb if @message.requested?(:disk_in_mb) + @process.log_rate_limit = @message.log_rate_limit_in_bytes_per_second if @message.requested?(:log_rate_limit_in_bytes_per_second) @process.save diff --git a/app/actions/space_diff_manifest.rb b/app/actions/space_diff_manifest.rb index 5a860535b46..2963896dd85 100644 --- a/app/actions/space_diff_manifest.rb +++ b/app/actions/space_diff_manifest.rb @@ -15,7 +15,7 @@ class << self def generate_diff(app_manifests, space) json_diff = [] recognized_top_level_keys = AppManifestMessage.allowed_keys.map(&:to_s) - app_manifests = convert_byte_measurements_to_mb(app_manifests) + app_manifests = normalize_units(app_manifests) app_manifests.each_with_index do |manifest_app_hash, index| manifest_app_hash = filter_manifest_app_hash(manifest_app_hash) existing_app = space.app_models.find { |app| app.name == manifest_app_hash['name'] } @@ -90,7 +90,7 @@ def filter_manifest_app_hash(manifest_app_hash) 'memory' ) end - manifest_app_hash['sidecars'] = convert_byte_measurements_to_mb(manifest_app_hash['sidecars']) + manifest_app_hash['sidecars'] = normalize_units(manifest_app_hash['sidecars']) manifest_app_hash = manifest_app_hash.except('sidecars') if manifest_app_hash['sidecars'] == [{}] end if manifest_app_hash.key? 'processes' @@ -99,6 +99,7 @@ def filter_manifest_app_hash(manifest_app_hash) 'type', 'command', 'disk_quota', + 'log_rate_limit_per_second', 'health-check-http-endpoint', 'health-check-invocation-timeout', 'health-check-type', @@ -107,7 +108,7 @@ def filter_manifest_app_hash(manifest_app_hash) 'timeout' ) end - manifest_app_hash['processes'] = convert_byte_measurements_to_mb(manifest_app_hash['processes']) + manifest_app_hash['processes'] = normalize_units(manifest_app_hash['processes']) manifest_app_hash = manifest_app_hash.except('processes') if manifest_app_hash['processes'] == [{}] end @@ -151,7 +152,7 @@ def create_similarity end end - def convert_byte_measurements_to_mb(manifest_app_hash) + def normalize_units(manifest_app_hash) byte_measurement_key_words = ['memory', 'disk-quota', 'disk_quota'] manifest_app_hash.each_with_index do |process_hash, index| byte_measurement_key_words.each do |key| @@ -159,6 +160,14 @@ def convert_byte_measurements_to_mb(manifest_app_hash) manifest_app_hash[index][key] = convert_to_mb(value, key) unless value.nil? end end + + byte_measurement_key_words = ['log_rate_limit_per_second'] + manifest_app_hash.each_with_index do |process_hash, index| + byte_measurement_key_words.each do |key| + value = process_hash[key] + manifest_app_hash[index][key] = normalize_unit(value, key) unless value.nil? + end + end manifest_app_hash end @@ -170,6 +179,14 @@ def convert_to_mb(human_readable_byte_value, attribute_name) "#{attribute_name} is not a number" end + def normalize_unit(non_normalized_value, attribute_name) + byte_converter.human_readable_byte_value(byte_converter.convert_to_b(non_normalized_value)) + rescue ByteConverter::InvalidUnitsError + "#{attribute_name} must use a supported unit: B, K, KB, M, MB, G, GB, T, or TB" + rescue ByteConverter::NonNumericError + "#{attribute_name} is not a number" + end + def byte_converter ByteConverter.new end diff --git a/app/actions/space_quota_update.rb b/app/actions/space_quota_update.rb index d20aee42a21..99c58cbee99 100644 --- a/app/actions/space_quota_update.rb +++ b/app/actions/space_quota_update.rb @@ -14,6 +14,7 @@ def self.update(quota, message) quota.instance_memory_limit = instance_memory_limit(message) if message.apps_limits_message.requested? :per_process_memory_in_mb quota.app_instance_limit = app_instance_limit(message) if message.apps_limits_message.requested? :total_instances quota.app_task_limit = app_task_limit(message) if message.apps_limits_message.requested? :per_app_tasks + quota.log_rate_limit = log_rate_limit(message) if message.apps_limits_message.requested? :log_rate_limit_in_bytes_per_second quota.total_services = total_services(message) if message.services_limits_message.requested? :total_service_instances quota.total_service_keys = total_service_keys(message) if message.services_limits_message.requested? :total_service_keys @@ -55,6 +56,10 @@ def self.app_task_limit(message) default_if_nil(message.per_app_tasks, SpaceQuotaDefinition::UNLIMITED) end + def self.log_rate_limit(message) + default_if_nil(message.log_rate_limit_in_bytes_per_second, SpaceQuotaDefinition::UNLIMITED) + end + def self.total_services(message) default_if_nil(message.total_service_instances, SpaceQuotaDefinition::UNLIMITED) end diff --git a/app/actions/space_quotas_create.rb b/app/actions/space_quotas_create.rb index 5b10fd617fd..fe33cfd1a4b 100644 --- a/app/actions/space_quotas_create.rb +++ b/app/actions/space_quotas_create.rb @@ -3,6 +3,7 @@ class SpaceQuotasCreate class Error < ::StandardError end + # rubocop:todo Metrics/CyclomaticComplexity def create(message, organization:) space_quota = nil @@ -16,6 +17,7 @@ def create(message, organization:) instance_memory_limit: message.per_process_memory_in_mb || SpaceQuotaDefinition::UNLIMITED, app_instance_limit: message.total_instances || SpaceQuotaDefinition::UNLIMITED, app_task_limit: message.per_app_tasks || SpaceQuotaDefinition::UNLIMITED, + log_rate_limit: message.log_rate_limit_in_bytes_per_second || QuotaDefinition::UNLIMITED, # Services total_services: message.total_service_instances || SpaceQuotaDefinition::DEFAULT_TOTAL_SERVICES, @@ -35,6 +37,7 @@ def create(message, organization:) rescue Sequel::ValidationFailed => e validation_error!(e, message) end + # rubocop:enable Metrics/CyclomaticComplexity private diff --git a/app/actions/task_create.rb b/app/actions/task_create.rb index 39301b6711b..dae42de0508 100644 --- a/app/actions/task_create.rb +++ b/app/actions/task_create.rb @@ -29,6 +29,7 @@ def create(app, message, user_audit_info, droplet: nil) command: command(message, template_process), disk_in_mb: disk_in_mb(message, template_process), memory_in_mb: memory_in_mb(message, template_process), + log_rate_limit: log_rate_limit(message, template_process), sequence_id: app.max_task_sequence_id ) @@ -70,15 +71,15 @@ def command(message, template_process) end def memory_in_mb(message, template_process) - return message.memory_in_mb if message.memory_in_mb + message.memory_in_mb || template_process.try(:memory) || config.get(:default_app_memory) + end - template_process.present? ? template_process.memory : config.get(:default_app_memory) + def log_rate_limit(message, template_process) + message.log_rate_limit_in_bytes_per_second || template_process.try(:log_rate_limit) || config.get(:default_app_log_rate_limit_in_bytes_per_second) end def disk_in_mb(message, template_process) - return message.disk_in_mb if message.disk_in_mb - - template_process.present? ? template_process.disk_quota : config.get(:default_app_disk_in_mb) + message.disk_in_mb || template_process.try(:disk_quota) || config.get(:default_app_disk_in_mb) end def submit_task(task) diff --git a/app/controllers/runtime/apps_controller.rb b/app/controllers/runtime/apps_controller.rb index d8d2d046e36..9e475490ad7 100644 --- a/app/controllers/runtime/apps_controller.rb +++ b/app/controllers/runtime/apps_controller.rb @@ -33,6 +33,7 @@ def self.dependencies attribute :docker_credentials, Hash, default: {} attribute :debug, String, default: nil attribute :disk_quota, Integer, default: nil + attribute :log_rate_limit, Integer, default: nil attribute :environment_json, Hash, default: {}, redact_in: [:create, :update] attribute :health_check_http_endpoint, String, default: nil attribute :health_check_type, String, default: 'port' diff --git a/app/controllers/v3/processes_controller.rb b/app/controllers/v3/processes_controller.rb index f02977d7eac..59f627d8f31 100644 --- a/app/controllers/v3/processes_controller.rb +++ b/app/controllers/v3/processes_controller.rb @@ -89,6 +89,7 @@ def scale 'instance-count' => message.instances, 'memory-in-mb' => message.memory_in_mb, 'disk-in-mb' => message.disk_in_mb, + 'log-rate-in-bytes-per-second' => message.log_rate_limit_in_bytes_per_second, 'process-type' => @process.type } ) diff --git a/app/messages/app_manifest_message.rb b/app/messages/app_manifest_message.rb index d4b02c257c2..f7589da5865 100644 --- a/app/messages/app_manifest_message.rb +++ b/app/messages/app_manifest_message.rb @@ -16,6 +16,7 @@ class AppManifestMessage < BaseMessage :buildpacks, :command, :disk_quota, + :log_rate_limit_per_second, :docker, :env, :health_check_http_endpoint, @@ -130,12 +131,16 @@ def manifest_buildpack_message end def process_scale_attribute_mappings - process_scale_attributes_from_app_level = process_scale_attributes(memory: memory, disk_quota: disk_quota, instances: instances) + process_scale_attributes_from_app_level = process_scale_attributes(memory: memory, + disk_quota: disk_quota, + log_rate_limit_per_second: log_rate_limit_per_second, + instances: instances) process_attributes(process_scale_attributes_from_app_level) do |process| process_scale_attributes( memory: process[:memory], disk_quota: process[:disk_quota], + log_rate_limit_per_second: process[:log_rate_limit_per_second], instances: process[:instances], type: process[:type] ) @@ -163,13 +168,15 @@ def process_attributes(app_attributes) process_attributes end - def process_scale_attributes(memory: nil, disk_quota: nil, instances:, type: nil) + def process_scale_attributes(memory: nil, disk_quota: nil, log_rate_limit_per_second: nil, instances:, type: nil) memory_in_mb = convert_to_mb(memory) disk_in_mb = convert_to_mb(disk_quota) + log_rate_limit_in_bytes_per_second = convert_to_bytes_per_second(log_rate_limit_per_second) { instances: instances, memory: memory_in_mb, disk_quota: disk_in_mb, + log_rate_limit: log_rate_limit_in_bytes_per_second, type: type }.compact end @@ -283,6 +290,13 @@ def convert_to_mb(human_readable_byte_value) rescue ByteConverter::InvalidUnitsError, ByteConverter::NonNumericError end + def convert_to_bytes_per_second(human_readable_byte_value) + return nil unless human_readable_byte_value.present? + + byte_converter.convert_to_b(human_readable_byte_value.strip) + rescue ByteConverter::InvalidUnitsError, ByteConverter::NonNumericError + end + def validate_byte_format(human_readable_byte_value, attribute_name) byte_converter.convert_to_mb(human_readable_byte_value) @@ -378,8 +392,10 @@ def validate_processes! type = process[:type] memory_error = validate_byte_format(process[:memory], 'Memory') disk_error = validate_byte_format(process[:disk_quota], 'Disk quota') + log_rate_limit_error = validate_byte_format(process[:log_rate_limit_per_second], 'Log rate limit per second') add_process_error!(memory_error, type) if memory_error add_process_error!(disk_error, type) if disk_error + add_process_error!(log_rate_limit_error, type) if log_rate_limit_error end end @@ -400,8 +416,10 @@ def validate_sidecars! def validate_top_level_web_process! memory_error = validate_byte_format(memory, 'Memory') disk_error = validate_byte_format(disk_quota, 'Disk quota') + log_rate_limit_error = validate_byte_format(log_rate_limit_per_second, 'Log rate limit per second') add_process_error!(memory_error, ProcessTypes::WEB) if memory_error add_process_error!(disk_error, ProcessTypes::WEB) if disk_error + add_process_error!(log_rate_limit_error, ProcessTypes::WEB) if log_rate_limit_error end def validate_buildpack_and_buildpacks_combination! diff --git a/app/messages/base_message.rb b/app/messages/base_message.rb index e798f14dce2..17922378621 100644 --- a/app/messages/base_message.rb +++ b/app/messages/base_message.rb @@ -8,6 +8,7 @@ class BaseMessage include Validators MAX_DB_INT = 2**31 - 1 + MAX_DB_BIGINT = 2**63 - 1 attr_accessor :requested_keys, :extra_keys diff --git a/app/messages/manifest_process_scale_message.rb b/app/messages/manifest_process_scale_message.rb index 6c916da69f6..67975869fb0 100644 --- a/app/messages/manifest_process_scale_message.rb +++ b/app/messages/manifest_process_scale_message.rb @@ -2,20 +2,24 @@ module VCAP::CloudController class ManifestProcessScaleMessage < BaseMessage - register_allowed_keys [:instances, :memory, :disk_quota, :type] + register_allowed_keys [:instances, :memory, :disk_quota, :log_rate_limit, :type] INVALID_MB_VALUE_ERROR = 'must be greater than 0MB'.freeze + # NOTE: -1 is valid for log_rate_limit representing unlimited + INVALID_QUOTA_VALUE_ERROR = 'must be an integer greater than or equal to -1'.freeze validates_with NoAdditionalKeysValidator validates :instances, numericality: { only_integer: true, greater_than_or_equal_to: 0, less_than_or_equal_to: 2000000 }, allow_nil: true validates :memory, numericality: { only_integer: true, greater_than: 0, message: INVALID_MB_VALUE_ERROR }, allow_nil: true validates :disk_quota, numericality: { only_integer: true, greater_than: 0, message: INVALID_MB_VALUE_ERROR }, allow_nil: true + validates :log_rate_limit, numericality: { only_integer: true, greater_than_or_equal_to: -1, message: INVALID_QUOTA_VALUE_ERROR }, allow_nil: true def to_process_scale_message ProcessScaleMessage.new({ instances: instances, memory_in_mb: memory, disk_in_mb: disk_quota, + log_rate_limit_in_bytes_per_second: log_rate_limit, }.compact) end end diff --git a/app/messages/organization_quotas_update_message.rb b/app/messages/organization_quotas_update_message.rb index b13c10ef29a..536a970266a 100644 --- a/app/messages/organization_quotas_update_message.rb +++ b/app/messages/organization_quotas_update_message.rb @@ -26,7 +26,7 @@ def self.key_requested?(key) validate :domains_validator, if: key_requested?(:domains) # Apps validations - delegate :total_memory_in_mb, :per_process_memory_in_mb, :total_instances, :per_app_tasks, to: :apps_limits_message + delegate :total_memory_in_mb, :per_process_memory_in_mb, :total_instances, :per_app_tasks, :log_rate_limit_in_bytes_per_second, to: :apps_limits_message def validates_hash(key, sym) return true if key.is_a?(Hash) diff --git a/app/messages/process_scale_message.rb b/app/messages/process_scale_message.rb index d57ca116b93..e6cd9f80804 100644 --- a/app/messages/process_scale_message.rb +++ b/app/messages/process_scale_message.rb @@ -2,12 +2,13 @@ module VCAP::CloudController class ProcessScaleMessage < BaseMessage - register_allowed_keys [:instances, :memory_in_mb, :disk_in_mb] + register_allowed_keys [:instances, :memory_in_mb, :disk_in_mb, :log_rate_limit_in_bytes_per_second] validates_with NoAdditionalKeysValidator validates :instances, numericality: { only_integer: true, greater_than_or_equal_to: 0, less_than_or_equal_to: MAX_DB_INT }, allow_nil: true validates :memory_in_mb, numericality: { only_integer: true, greater_than: 0, less_than_or_equal_to: MAX_DB_INT }, allow_nil: true validates :disk_in_mb, numericality: { only_integer: true, greater_than: 0, less_than_or_equal_to: MAX_DB_INT }, allow_nil: true + validates :log_rate_limit_in_bytes_per_second, numericality: { only_integer: true, greater_than_or_equal_to: -1, less_than_or_equal_to: MAX_DB_BIGINT }, allow_nil: true end end diff --git a/app/messages/quotas_apps_message.rb b/app/messages/quotas_apps_message.rb index fe488dc319c..e1029133dc8 100644 --- a/app/messages/quotas_apps_message.rb +++ b/app/messages/quotas_apps_message.rb @@ -3,7 +3,7 @@ module VCAP::CloudController class QuotasAppsMessage < BaseMessage - register_allowed_keys [:total_memory_in_mb, :per_process_memory_in_mb, :total_instances, :per_app_tasks] + register_allowed_keys [:total_memory_in_mb, :per_process_memory_in_mb, :total_instances, :per_app_tasks, :log_rate_limit_in_bytes_per_second] validates_with NoAdditionalKeysValidator @@ -22,5 +22,9 @@ class QuotasAppsMessage < BaseMessage validates :per_app_tasks, numericality: { only_integer: true, greater_than_or_equal_to: 0, less_than_or_equal_to: MAX_DB_INT }, allow_nil: true + + validates :log_rate_limit_in_bytes_per_second, + numericality: { only_integer: true, greater_than_or_equal_to: 0, less_than_or_equal_to: MAX_DB_BIGINT }, + allow_nil: true end end diff --git a/app/messages/space_quota_update_message.rb b/app/messages/space_quota_update_message.rb index dbb6728488d..de1f3c51fd3 100644 --- a/app/messages/space_quota_update_message.rb +++ b/app/messages/space_quota_update_message.rb @@ -22,7 +22,7 @@ def self.key_requested?(key) validate :services_validator, if: key_requested?(:services) validate :routes_validator, if: key_requested?(:routes) - delegate :total_memory_in_mb, :per_process_memory_in_mb, :total_instances, :per_app_tasks, to: :apps_limits_message + delegate :total_memory_in_mb, :per_process_memory_in_mb, :total_instances, :per_app_tasks, :log_rate_limit_in_bytes_per_second, to: :apps_limits_message delegate :paid_services_allowed, :total_service_instances, :total_service_keys, to: :services_limits_message delegate :total_routes, :total_reserved_ports, to: :routes_limits_message diff --git a/app/messages/task_create_message.rb b/app/messages/task_create_message.rb index 5f230151e5d..32f90554a80 100644 --- a/app/messages/task_create_message.rb +++ b/app/messages/task_create_message.rb @@ -2,7 +2,7 @@ module VCAP::CloudController class TaskCreateMessage < MetadataBaseMessage - register_allowed_keys [:name, :command, :disk_in_mb, :memory_in_mb, :droplet_guid, :template] + register_allowed_keys [:name, :command, :disk_in_mb, :memory_in_mb, :log_rate_limit_in_bytes_per_second, :droplet_guid, :template] validates_with NoAdditionalKeysValidator @@ -12,6 +12,7 @@ def self.validate_template? validates :disk_in_mb, numericality: { only_integer: true, greater_than: 0 }, allow_nil: true validates :memory_in_mb, numericality: { only_integer: true, greater_than: 0 }, allow_nil: true + validates :log_rate_limit_in_bytes_per_second, numericality: { only_integer: true, greater_than: -2 }, allow_nil: true validates :droplet_guid, guid: true, allow_nil: true validates :template_process_guid, guid: true, if: validate_template? validate :has_command diff --git a/app/models.rb b/app/models.rb index 6c1fe448fb6..34672088446 100644 --- a/app/models.rb +++ b/app/models.rb @@ -47,6 +47,8 @@ require 'models/runtime/quota_constraints/max_service_keys_policy' require 'models/runtime/constraints/max_disk_quota_policy' require 'models/runtime/constraints/min_disk_quota_policy' +require 'models/runtime/constraints/min_log_rate_limit_policy' +require 'models/runtime/constraints/max_log_rate_limit_policy' require 'models/runtime/constraints/max_memory_policy' require 'models/runtime/constraints/max_instance_memory_policy' require 'models/runtime/constraints/min_memory_policy' diff --git a/app/models/runtime/constraints/max_log_rate_limit_policy.rb b/app/models/runtime/constraints/max_log_rate_limit_policy.rb new file mode 100644 index 00000000000..a5a6ea14c0a --- /dev/null +++ b/app/models/runtime/constraints/max_log_rate_limit_policy.rb @@ -0,0 +1,78 @@ +require 'cloud_controller/app_services/process_log_rate_limit_calculator' + +class BaseMaxLogRateLimitPolicy + def initialize(resource, policy_target, error_name) + @resource = resource + @policy_target = policy_target + @error_name = error_name + end + + def validate + return unless policy_target + return unless additional_checks + + if requested_log_rate_limit == VCAP::CloudController::QuotaDefinition::UNLIMITED && + policy_target.log_rate_limit != VCAP::CloudController::QuotaDefinition::UNLIMITED + + policy_target_type = if policy_target.respond_to?(:organization_guid) + 'space' + else + 'organization' + end + + resource.errors.add(field, "cannot be unlimited in #{policy_target_type} '#{policy_target.name}'.") + end + + unless policy_target.has_remaining_log_rate_limit(requested_log_rate_limit) + resource.errors.add(field, error_name) + end + end + + private + + attr_reader :resource, :policy_target, :error_name + + def additional_checks + true + end + + def requested_log_rate_limit + resource.public_send field + end + + def field + :log_rate_limit + end +end + +class AppMaxLogRateLimitPolicy < BaseMaxLogRateLimitPolicy + private + + def additional_checks + resource.started? && + (resource.column_changed?(:state) || resource.column_changed?(:instances)) + end + + def requested_log_rate_limit + calculator = VCAP::CloudController::ProcessLogRateLimitCalculator.new(resource) + calculator.additional_log_rate_limit_requested + end +end + +class TaskMaxLogRateLimitPolicy < BaseMaxLogRateLimitPolicy + IGNORED_STATES = [ + VCAP::CloudController::TaskModel::CANCELING_STATE, + VCAP::CloudController::TaskModel::SUCCEEDED_STATE, + VCAP::CloudController::TaskModel::FAILED_STATE, + ].freeze + + private + + def additional_checks + IGNORED_STATES.exclude?(resource.state) + end + + def field + :log_rate_limit + end +end diff --git a/app/models/runtime/constraints/min_log_rate_limit_policy.rb b/app/models/runtime/constraints/min_log_rate_limit_policy.rb new file mode 100644 index 00000000000..62c2dbfdb66 --- /dev/null +++ b/app/models/runtime/constraints/min_log_rate_limit_policy.rb @@ -0,0 +1,16 @@ +class MinLogRateLimitPolicy + ERROR_MSG = 'log_rate_limit must be greater than or equal to -1 (where -1 is unlimited)'.freeze + + def initialize(process) + @process = process + @errors = process.errors + end + + def validate + return unless @process.log_rate_limit + + if @process.log_rate_limit < -1 + @errors.add(:log_rate_limit, ERROR_MSG) + end + end +end diff --git a/app/models/runtime/organization.rb b/app/models/runtime/organization.rb index 661f5382e56..755257323e4 100644 --- a/app/models/runtime/organization.rb +++ b/app/models/runtime/organization.rb @@ -230,13 +230,21 @@ def memory_used end def has_remaining_memory(mem) - quota_definition.memory_limit == -1 || memory_remaining >= mem + quota_definition.memory_limit == QuotaDefinition::UNLIMITED || memory_remaining >= mem + end + + def has_remaining_log_rate_limit(log_rate_limit_desired) + quota_definition.log_rate_limit == QuotaDefinition::UNLIMITED || log_rate_limit_remaining >= log_rate_limit_desired end def instance_memory_limit quota_definition ? quota_definition.instance_memory_limit : QuotaDefinition::UNLIMITED end + def log_rate_limit + quota_definition ? quota_definition.log_rate_limit : QuotaDefinition::UNLIMITED + end + def app_task_limit quota_definition ? quota_definition.app_task_limit : QuotaDefinition::UNLIMITED end @@ -314,6 +322,10 @@ def memory_remaining quota_definition.memory_limit - memory_used end + def log_rate_limit_remaining + quota_definition.log_rate_limit - (started_app_log_rate_limit + running_task_log_rate_limit) + end + def running_task_memory tasks_dataset.where(state: TaskModel::RUNNING_STATE).sum(:memory_in_mb) || 0 end @@ -322,6 +334,14 @@ def started_app_memory processes_dataset.where(state: ProcessModel::STARTED).sum(Sequel.*(:memory, :instances)) || 0 end + def running_task_log_rate_limit + tasks_dataset.where(state: TaskModel::RUNNING_STATE).sum(:log_rate_limit) || 0 + end + + def started_app_log_rate_limit + processes_dataset.where(state: ProcessModel::STARTED).sum(Sequel.*(:log_rate_limit, :instances)) || 0 + end + def running_and_pending_tasks_count tasks_dataset.where(state: [TaskModel::PENDING_STATE, TaskModel::RUNNING_STATE]).count end diff --git a/app/models/runtime/process_model.rb b/app/models/runtime/process_model.rb index f454a0ed73e..a76b5172163 100644 --- a/app/models/runtime/process_model.rb +++ b/app/models/runtime/process_model.rb @@ -27,6 +27,7 @@ def after_initialize self.memory ||= Config.config.get(:default_app_memory) self.disk_quota ||= Config.config.get(:default_app_disk_in_mb) self.file_descriptors ||= Config.config.get(:instance_file_descriptor_limit) + self.log_rate_limit ||= Config.config.get(:default_app_log_rate_limit_in_bytes_per_second) self.metadata ||= {} end @@ -144,17 +145,19 @@ def non_docker_type add_association_dependencies annotations: :destroy export_attributes :name, :production, :space_guid, :stack_guid, :buildpack, - :detected_buildpack, :detected_buildpack_guid, :environment_json, :memory, :instances, :disk_quota, - :state, :version, :command, :console, :debug, :staging_task_id, - :package_state, :health_check_type, :health_check_timeout, :health_check_http_endpoint, - :staging_failed_reason, :staging_failed_description, :diego, :docker_image, :package_updated_at, - :detected_start_command, :enable_ssh, :ports + :detected_buildpack, :detected_buildpack_guid, :environment_json, + :memory, :instances, :disk_quota, :log_rate_limit, :state, :version, :command, + :console, :debug, :staging_task_id, :package_state, :health_check_type, + :health_check_timeout, :health_check_http_endpoint, :staging_failed_reason, + :staging_failed_description, :diego, :docker_image, :package_updated_at, + :detected_start_command, :enable_ssh, :ports import_attributes :name, :production, :space_guid, :stack_guid, :buildpack, :detected_buildpack, :environment_json, :memory, :instances, :disk_quota, - :state, :command, :console, :debug, :staging_task_id, - :service_binding_guids, :route_guids, :health_check_type, :health_check_http_endpoint, - :health_check_timeout, :diego, :docker_image, :app_guid, :enable_ssh, :ports + :log_rate_limit, :state, :command, :console, :debug, :staging_task_id, + :service_binding_guids, :route_guids, :health_check_type, + :health_check_http_endpoint, :health_check_timeout, :diego, + :docker_image, :app_guid, :enable_ssh, :ports serialize_attributes :json, :metadata serialize_attributes :integer_array, :ports @@ -258,6 +261,9 @@ def validation_policies InstancesPolicy.new(self), MaxAppInstancesPolicy.new(self, organization, organization && organization.quota_definition, :app_instance_limit_exceeded), MaxAppInstancesPolicy.new(self, space, space && space.space_quota_definition, :space_app_instance_limit_exceeded), + MinLogRateLimitPolicy.new(self), + AppMaxLogRateLimitPolicy.new(self, space, 'exceeds space log rate quota'), + AppMaxLogRateLimitPolicy.new(self, organization, 'exceeds organization log rate quota'), HealthCheckPolicy.new(self, health_check_timeout, health_check_invocation_timeout), DockerPolicy.new(self), PortsPolicy.new(self) diff --git a/app/models/runtime/quota_definition.rb b/app/models/runtime/quota_definition.rb index 81374dda85d..b4d24cae2c2 100644 --- a/app/models/runtime/quota_definition.rb +++ b/app/models/runtime/quota_definition.rb @@ -12,12 +12,13 @@ class QuotaDefinition < Sequel::Model export_attributes :name, :non_basic_services_allowed, :total_services, :total_routes, :total_private_domains, :memory_limit, :trial_db_allowed, :instance_memory_limit, - :app_instance_limit, :app_task_limit, :total_service_keys, :total_reserved_route_ports + :app_instance_limit, :app_task_limit, :total_service_keys, :total_reserved_route_ports, + :log_rate_limit import_attributes :name, :non_basic_services_allowed, :total_services, :total_routes, :total_private_domains, :memory_limit, :trial_db_allowed, :instance_memory_limit, - :app_instance_limit, :app_task_limit, :total_service_keys, :total_reserved_route_ports + :app_instance_limit, :app_task_limit, :total_service_keys, :total_reserved_route_ports, + :log_rate_limit - # rubocop:disable Metrics/CyclomaticComplexity def validate validates_presence :name validates_unique :name @@ -27,14 +28,14 @@ def validate validates_presence :memory_limit validate_total_reserved_route_ports - errors.add(:memory_limit, :invalid_memory_limit) if memory_limit && memory_limit < UNLIMITED - errors.add(:instance_memory_limit, :invalid_instance_memory_limit) if instance_memory_limit && instance_memory_limit < UNLIMITED - errors.add(:total_private_domains, :invalid_total_private_domains) if total_private_domains && total_private_domains < UNLIMITED - errors.add(:app_instance_limit, :invalid_app_instance_limit) if app_instance_limit && app_instance_limit < UNLIMITED - errors.add(:app_task_limit, :invalid_app_task_limit) if app_task_limit && app_task_limit < UNLIMITED - errors.add(:total_service_keys, :invalid_total_service_keys) if total_service_keys && total_service_keys < UNLIMITED + validates_limit(:memory_limit, memory_limit) + validates_limit(:instance_memory_limit, instance_memory_limit) + validates_limit(:total_private_domains, total_private_domains) + validates_limit(:app_instance_limit, app_instance_limit) + validates_limit(:app_task_limit, app_task_limit) + validates_limit(:log_rate_limit, log_rate_limit) + validates_limit(:total_service_keys, total_service_keys) end - # rubocop:enable Metrics/CyclomaticComplexity def before_destroy if organizations.present? @@ -66,6 +67,10 @@ def self.user_visibility_filter(user) private + def validates_limit(limit_name, limit) + errors.add(limit_name, :"invalid_#{limit_name}") if limit && limit < UNLIMITED + end + def validate_total_reserved_route_ports return unless total_reserved_route_ports diff --git a/app/models/runtime/space.rb b/app/models/runtime/space.rb index a7566a84f9d..65daf1b7fb6 100644 --- a/app/models/runtime/space.rb +++ b/app/models/runtime/space.rb @@ -286,6 +286,12 @@ def has_remaining_memory(mem) space_quota_definition.memory_limit == SpaceQuotaDefinition::UNLIMITED || memory_remaining >= mem end + def has_remaining_log_rate_limit(log_rate_limit_desired) + return true unless space_quota_definition + + space_quota_definition.log_rate_limit == SpaceQuotaDefinition::UNLIMITED || log_rate_limit_remaining >= log_rate_limit_desired + end + def instance_memory_limit if space_quota_definition space_quota_definition.instance_memory_limit @@ -294,6 +300,14 @@ def instance_memory_limit end end + def log_rate_limit + if space_quota_definition + space_quota_definition.log_rate_limit + else + SpaceQuotaDefinition::UNLIMITED + end + end + def app_task_limit if space_quota_definition space_quota_definition.app_task_limit @@ -329,6 +343,10 @@ def memory_remaining space_quota_definition.memory_limit - memory_used end + def log_rate_limit_remaining + space_quota_definition.log_rate_limit - (started_app_log_rate_limit + running_task_log_rate_limit) + end + def running_task_memory tasks_dataset.where(state: TaskModel::RUNNING_STATE).sum(:memory_in_mb) || 0 end @@ -337,6 +355,14 @@ def started_app_memory processes_dataset.where(state: ProcessModel::STARTED).sum(Sequel.*(:memory, :instances)) || 0 end + def running_task_log_rate_limit + tasks_dataset.where(state: TaskModel::RUNNING_STATE).sum(:log_rate_limit) || 0 + end + + def started_app_log_rate_limit + processes_dataset.where(state: ProcessModel::STARTED).sum(Sequel.*(:log_rate_limit, :instances)) || 0 + end + def running_and_pending_tasks_count tasks_dataset.where(state: [TaskModel::PENDING_STATE, TaskModel::RUNNING_STATE]).count end diff --git a/app/models/runtime/space_quota_definition.rb b/app/models/runtime/space_quota_definition.rb index 4be195c6a4d..bb0668cc42d 100644 --- a/app/models/runtime/space_quota_definition.rb +++ b/app/models/runtime/space_quota_definition.rb @@ -18,10 +18,10 @@ class SpaceQuotaDefinition < Sequel::Model export_attributes :name, :organization_guid, :non_basic_services_allowed, :total_services, :total_routes, :memory_limit, :instance_memory_limit, :app_instance_limit, :app_task_limit, - :total_service_keys, :total_reserved_route_ports + :total_service_keys, :total_reserved_route_ports, :log_rate_limit import_attributes :name, :organization_guid, :non_basic_services_allowed, :total_services, :total_routes, :memory_limit, :instance_memory_limit, :app_instance_limit, :app_task_limit, - :total_service_keys, :total_reserved_route_ports + :total_service_keys, :total_reserved_route_ports, :log_rate_limit add_association_dependencies spaces: :nullify @@ -34,11 +34,13 @@ def validate validates_presence :organization validates_unique [:organization_id, :name] - errors.add(:memory_limit, :invalid_memory_limit) if memory_limit && memory_limit < UNLIMITED - errors.add(:instance_memory_limit, :invalid_instance_memory_limit) if instance_memory_limit && instance_memory_limit < -1 - errors.add(:app_instance_limit, :invalid_app_instance_limit) if app_instance_limit && app_instance_limit < UNLIMITED - errors.add(:app_task_limit, :invalid_app_task_limit) if app_task_limit && app_task_limit < UNLIMITED - errors.add(:total_service_keys, :invalid_total_service_keys) if total_service_keys && total_service_keys < UNLIMITED + validates_limit(:memory_limit, memory_limit) + validates_limit(:instance_memory_limit, instance_memory_limit) + validates_limit(:app_instance_limit, app_instance_limit) + validates_limit(:app_task_limit, app_task_limit) + validates_limit(:log_rate_limit, log_rate_limit) + validates_limit(:total_service_keys, total_service_keys) + validate_total_reserved_ports end @@ -58,6 +60,10 @@ def self.user_visibility_filter(user) private + def validates_limit(limit_name, limit) + errors.add(limit_name, :"invalid_#{limit_name}") if limit && limit < UNLIMITED + end + def validate_total_reserved_ports return unless total_reserved_route_ports diff --git a/app/models/runtime/task_model.rb b/app/models/runtime/task_model.rb index c3541bb9bdd..be692b0f5c2 100644 --- a/app/models/runtime/task_model.rb +++ b/app/models/runtime/task_model.rb @@ -67,17 +67,21 @@ def validate validates_presence :name validate_org_quotas validate_space_quotas + + MinLogRateLimitPolicy.new(self).validate end def validate_space_quotas TaskMaxMemoryPolicy.new(self, space, 'exceeds space memory quota').validate TaskMaxInstanceMemoryPolicy.new(self, space, 'exceeds space instance memory quota').validate + TaskMaxLogRateLimitPolicy.new(self, space, 'exceeds space log rate quota').validate new? && MaxAppTasksPolicy.new(self, space, 'quota exceeded').validate end def validate_org_quotas TaskMaxMemoryPolicy.new(self, organization, 'exceeds organization memory quota').validate TaskMaxInstanceMemoryPolicy.new(self, organization, 'exceeds organization instance memory quota').validate + TaskMaxLogRateLimitPolicy.new(self, organization, 'exceeds organization log rate quota').validate new? && MaxAppTasksPolicy.new(self, organization, 'quota exceeded').validate end diff --git a/app/presenters/v2/process_model_presenter.rb b/app/presenters/v2/process_model_presenter.rb index 956ff1b3b77..a87330d54d5 100644 --- a/app/presenters/v2/process_model_presenter.rb +++ b/app/presenters/v2/process_model_presenter.rb @@ -21,6 +21,7 @@ def entity_hash(controller, process, opts, depth, parents, orphans=nil) 'memory' => process.memory, 'instances' => process.instances, 'disk_quota' => process.disk_quota, + 'log_rate_limit' => process.log_rate_limit, 'state' => process.state, 'version' => process.version, 'command' => process.command.presence, diff --git a/app/presenters/v3/app_manifest_presenters/process_properties_presenter.rb b/app/presenters/v3/app_manifest_presenters/process_properties_presenter.rb index 0535df9a8e5..155a5386976 100644 --- a/app/presenters/v3/app_manifest_presenters/process_properties_presenter.rb +++ b/app/presenters/v3/app_manifest_presenters/process_properties_presenter.rb @@ -14,6 +14,7 @@ def process_hash(process) 'instances' => process.instances, 'memory' => add_units(process.memory), 'disk_quota' => add_units(process.disk_quota), + 'log_rate_limit_per_second' => add_units_log_rate_limit(process.log_rate_limit), 'command' => process.command, 'health-check-type' => process.health_check_type, 'health-check-http-endpoint' => process.health_check_http_endpoint, @@ -24,6 +25,18 @@ def process_hash(process) def add_units(val) "#{val}M" end + + def add_units_log_rate_limit(val) + if val == -1 + '-1B' + else + byte_converter.human_readable_byte_value(val) + end + end + + def byte_converter + ByteConverter.new + end end end end diff --git a/app/presenters/v3/organization_quota_presenter.rb b/app/presenters/v3/organization_quota_presenter.rb index 5f0d1593a65..f4c15b04434 100644 --- a/app/presenters/v3/organization_quota_presenter.rb +++ b/app/presenters/v3/organization_quota_presenter.rb @@ -26,6 +26,7 @@ def to_hash per_process_memory_in_mb: convert_unlimited_to_nil(organization_quota.instance_memory_limit), total_instances: convert_unlimited_to_nil(organization_quota.app_instance_limit), per_app_tasks: convert_unlimited_to_nil(organization_quota.app_task_limit), + log_rate_limit_in_bytes_per_second: convert_unlimited_to_nil(organization_quota.log_rate_limit), }, services: { paid_services_allowed: organization_quota.non_basic_services_allowed, diff --git a/app/presenters/v3/process_presenter.rb b/app/presenters/v3/process_presenter.rb index 26d0f7fb2ce..d68eb4f5c5c 100644 --- a/app/presenters/v3/process_presenter.rb +++ b/app/presenters/v3/process_presenter.rb @@ -19,14 +19,15 @@ def to_hash health_check_data = { timeout: process.health_check_timeout, invocation_timeout: process.health_check_invocation_timeout } health_check_data[:endpoint] = process.health_check_http_endpoint if process.health_check_type == HealthCheckTypes::HTTP { - guid: process.guid, - created_at: process.created_at, - updated_at: process.updated_at, - type: process.type, - command: redact(process.specified_or_detected_command), - instances: process.instances, - memory_in_mb: process.memory, - disk_in_mb: process.disk_quota, + guid: process.guid, + created_at: process.created_at, + updated_at: process.updated_at, + type: process.type, + command: redact(process.specified_or_detected_command), + instances: process.instances, + memory_in_mb: process.memory, + disk_in_mb: process.disk_quota, + log_rate_limit_in_bytes_per_second: process.log_rate_limit, health_check: { type: process.health_check_type, data: health_check_data diff --git a/app/presenters/v3/process_stats_presenter.rb b/app/presenters/v3/process_stats_presenter.rb index 47fbc14802d..4c17c083c62 100644 --- a/app/presenters/v3/process_stats_presenter.rb +++ b/app/presenters/v3/process_stats_presenter.rb @@ -39,6 +39,7 @@ def found_instance_stats_hash(index, stats) uptime: stats[:stats][:uptime], mem_quota: stats[:stats][:mem_quota], disk_quota: stats[:stats][:disk_quota], + log_rate_limit: stats[:stats][:log_rate_limit], fds_quota: stats[:stats][:fds_quota], isolation_segment: stats[:isolation_segment], details: stats[:details] @@ -74,6 +75,7 @@ def add_usage_info(presented_stats, stats) cpu: stats[:stats][:usage][:cpu], mem: stats[:stats][:usage][:mem], disk: stats[:stats][:usage][:disk], + log_rate: stats[:stats][:usage][:log_rate], } else {} diff --git a/app/presenters/v3/space_quota_presenter.rb b/app/presenters/v3/space_quota_presenter.rb index 1c1b3ab2a8d..4d4da24c28e 100644 --- a/app/presenters/v3/space_quota_presenter.rb +++ b/app/presenters/v3/space_quota_presenter.rb @@ -26,6 +26,7 @@ def to_hash per_process_memory_in_mb: unlimited_to_nil(space_quota.instance_memory_limit), total_instances: unlimited_to_nil(space_quota.app_instance_limit), per_app_tasks: unlimited_to_nil(space_quota.app_task_limit), + log_rate_limit_in_bytes_per_second: unlimited_to_nil(space_quota.log_rate_limit), }, services: { paid_services_allowed: space_quota.non_basic_services_allowed, diff --git a/app/presenters/v3/task_presenter.rb b/app/presenters/v3/task_presenter.rb index 616a9ec99b6..2ba114fc33a 100644 --- a/app/presenters/v3/task_presenter.rb +++ b/app/presenters/v3/task_presenter.rb @@ -18,6 +18,7 @@ def to_hash state: task.state, memory_in_mb: task.memory_in_mb, disk_in_mb: task.disk_in_mb, + log_rate_limit_in_bytes_per_second: task.log_rate_limit, result: { failure_reason: task.failure_reason }, droplet_guid: task.droplet_guid, relationships: { app: { data: { guid: task.app_guid } } }, diff --git a/config/bosh-lite.yml b/config/bosh-lite.yml index 3931c6bec2a..c2c3f001251 100644 --- a/config/bosh-lite.yml +++ b/config/bosh-lite.yml @@ -36,6 +36,7 @@ completed_tasks: default_app_memory: 256 default_app_disk_in_mb: 1024 +default_app_log_rate_limit_in_bytes_per_second: 1_048_576 maximum_app_disk_in_mb: 2048 instance_file_descriptor_limit: 16384 diff --git a/config/cloud_controller.yml b/config/cloud_controller.yml index eb3e61c0249..3609f71c879 100644 --- a/config/cloud_controller.yml +++ b/config/cloud_controller.yml @@ -53,6 +53,7 @@ completed_tasks: default_app_memory: 1024 #mb default_app_disk_in_mb: 1024 +default_app_log_rate_limit_in_bytes_per_second: 1_048_576 maximum_app_disk_in_mb: 2048 max_retained_deployments_per_app: 100 max_retained_builds_per_app: 100 diff --git a/db/migrations/20220606172913_add_log_rate_limit_to_quota_definitions.rb b/db/migrations/20220606172913_add_log_rate_limit_to_quota_definitions.rb new file mode 100644 index 00000000000..475e005dc17 --- /dev/null +++ b/db/migrations/20220606172913_add_log_rate_limit_to_quota_definitions.rb @@ -0,0 +1,5 @@ +Sequel.migration do + change do + add_column :quota_definitions, :log_rate_limit, :Bignum, null: false, default: -1 + end +end diff --git a/db/migrations/20220607222309_add_log_rate_limit_to_space_quota_definitions.rb b/db/migrations/20220607222309_add_log_rate_limit_to_space_quota_definitions.rb new file mode 100644 index 00000000000..4abca30f194 --- /dev/null +++ b/db/migrations/20220607222309_add_log_rate_limit_to_space_quota_definitions.rb @@ -0,0 +1,5 @@ +Sequel.migration do + change do + add_column :space_quota_definitions, :log_rate_limit, :Bignum, null: false, default: -1 + end +end diff --git a/db/migrations/20220608165055_add_log_rate_limit_to_processes.rb b/db/migrations/20220608165055_add_log_rate_limit_to_processes.rb new file mode 100644 index 00000000000..d2825f53908 --- /dev/null +++ b/db/migrations/20220608165055_add_log_rate_limit_to_processes.rb @@ -0,0 +1,5 @@ +Sequel.migration do + change do + add_column :processes, :log_rate_limit, :Bignum, null: false, default: -1 + end +end diff --git a/db/migrations/20220621175820_add_log_rate_limit_to_tasks_table.rb b/db/migrations/20220621175820_add_log_rate_limit_to_tasks_table.rb new file mode 100644 index 00000000000..be1062df2b1 --- /dev/null +++ b/db/migrations/20220621175820_add_log_rate_limit_to_tasks_table.rb @@ -0,0 +1,5 @@ +Sequel.migration do + change do + add_column :tasks, :log_rate_limit, :Bignum, null: false, default: -1 + end +end diff --git a/lib/cloud_controller/app_manifest/byte_converter.rb b/lib/cloud_controller/app_manifest/byte_converter.rb index e5c2828083b..1dbf589367e 100644 --- a/lib/cloud_controller/app_manifest/byte_converter.rb +++ b/lib/cloud_controller/app_manifest/byte_converter.rb @@ -2,6 +2,7 @@ module VCAP::CloudController class ByteConverter + class InvalidBytesError < StandardError; end class InvalidUnitsError < StandardError; end class NonNumericError < StandardError; end @@ -15,5 +16,34 @@ def convert_to_mb(human_readable_byte_value) rescue PalmCivet::InvalidByteQuantityError raise InvalidUnitsError end + + def convert_to_b(human_readable_byte_value) + return nil unless human_readable_byte_value.present? + if !human_readable_byte_value.to_s.match?(/\A-?\d+(?:\.\d+)?/) + raise NonNumericError + end + + PalmCivet.to_bytes(human_readable_byte_value.to_s) + rescue PalmCivet::InvalidByteQuantityError + raise InvalidUnitsError + end + + def human_readable_byte_value(bytes) + return nil unless bytes.present? + + if !bytes.is_a?(Integer) + raise InvalidBytesError + end + + units = %w(B K M G T) + while units.any? + unit_in_bytes = 1024**(units.length - 1) + if bytes.remainder(unit_in_bytes).zero? + return "#{bytes / unit_in_bytes}#{units.last}" + end + + units.pop + end + end end end diff --git a/lib/cloud_controller/app_services/process_log_rate_limit_calculator.rb b/lib/cloud_controller/app_services/process_log_rate_limit_calculator.rb new file mode 100644 index 00000000000..eae51fec2d2 --- /dev/null +++ b/lib/cloud_controller/app_services/process_log_rate_limit_calculator.rb @@ -0,0 +1,51 @@ +module VCAP::CloudController + class ProcessLogRateLimitCalculator + attr_reader :process + + def initialize(process) + @process = process + end + + def additional_log_rate_limit_requested + return 0 if process.stopped? + return QuotaDefinition::UNLIMITED if is_process_log_rate_unlimited? + + total_requested_log_rate_limit - currently_used_log_rate_limit + end + + def total_requested_log_rate_limit + return 0 if process.log_rate_limit == QuotaDefinition::UNLIMITED + + process.log_rate_limit * process.instances + end + + def currently_used_log_rate_limit + return 0 if process.new? + + db_process = process_from_db + return 0 if db_process.stopped? || db_process[:log_rate_limit] == QuotaDefinition::UNLIMITED + + db_process[:log_rate_limit] * db_process[:instances] + end + + private + + def is_process_log_rate_unlimited? + process.log_rate_limit == QuotaDefinition::UNLIMITED + end + + def process_from_db + error_message = 'Expected process record not found in database with guid %s' + process_fetched_from_db = ProcessModel.find(guid: process.guid) + if process_fetched_from_db.nil? + logger.fatal('process.find.missing', guid: process.guid, self: process.inspect) + raise CloudController::Errors::ApplicationMissing.new(sprintf(error_message, guid: process.guid)) + end + process_fetched_from_db + end + + def logger + @logger ||= Steno.logger('cc.process_log_rate_limit_calculator') + end + end +end diff --git a/lib/cloud_controller/config_schemas/base/api_schema.rb b/lib/cloud_controller/config_schemas/base/api_schema.rb index 6438bc707ae..157e86b6f09 100644 --- a/lib/cloud_controller/config_schemas/base/api_schema.rb +++ b/lib/cloud_controller/config_schemas/base/api_schema.rb @@ -38,6 +38,7 @@ class ApiSchema < VCAP::Config default_app_memory: Integer, default_app_disk_in_mb: Integer, + default_app_log_rate_limit_in_bytes_per_second: Integer, maximum_app_disk_in_mb: Integer, default_health_check_timeout: Integer, maximum_health_check_timeout: Integer, diff --git a/lib/cloud_controller/config_schemas/base/clock_schema.rb b/lib/cloud_controller/config_schemas/base/clock_schema.rb index 92f50b6a54e..10f14a576cf 100644 --- a/lib/cloud_controller/config_schemas/base/clock_schema.rb +++ b/lib/cloud_controller/config_schemas/base/clock_schema.rb @@ -155,6 +155,7 @@ class ClockSchema < VCAP::Config default_app_memory: Integer, default_app_disk_in_mb: Integer, + default_app_log_rate_limit_in_bytes_per_second: Integer, instance_file_descriptor_limit: Integer, maximum_app_disk_in_mb: Integer, max_retained_deployments_per_app: Integer, diff --git a/lib/cloud_controller/config_schemas/base/worker_schema.rb b/lib/cloud_controller/config_schemas/base/worker_schema.rb index ceb64038f3c..dadb554bde2 100644 --- a/lib/cloud_controller/config_schemas/base/worker_schema.rb +++ b/lib/cloud_controller/config_schemas/base/worker_schema.rb @@ -166,6 +166,7 @@ class WorkerSchema < VCAP::Config default_app_disk_in_mb: Integer, instance_file_descriptor_limit: Integer, maximum_app_disk_in_mb: Integer, + default_app_log_rate_limit_in_bytes_per_second: Integer, default_app_ssh_access: bool, jobs: { diff --git a/spec/request/app_manifests_spec.rb b/spec/request/app_manifests_spec.rb index 5ccb23e9ca4..1fe2ae2a693 100644 --- a/spec/request/app_manifests_spec.rb +++ b/spec/request/app_manifests_spec.rb @@ -39,6 +39,7 @@ health_check_type: 'http', health_check_http_endpoint: '/foobar', health_check_timeout: 5, + log_rate_limit: 1_048_576, ) end @@ -107,6 +108,7 @@ 'instances' => process.instances, 'memory' => "#{process.memory}M", 'disk_quota' => "#{process.disk_quota}M", + 'log_rate_limit_per_second' => '1M', 'health-check-type' => process.health_check_type, }, { @@ -114,6 +116,7 @@ 'instances' => worker_process.instances, 'memory' => "#{worker_process.memory}M", 'disk_quota' => "#{worker_process.disk_quota}M", + 'log_rate_limit_per_second' => '1M', 'command' => worker_process.command, 'health-check-type' => worker_process.health_check_type, 'health-check-http-endpoint' => worker_process.health_check_http_endpoint, @@ -170,6 +173,7 @@ before do app_model.update(droplet: droplet) + process.update(log_rate_limit: -1) end let(:expected_yml_manifest) do @@ -203,6 +207,7 @@ 'instances' => process.instances, 'memory' => "#{process.memory}M", 'disk_quota' => "#{process.disk_quota}M", + 'log_rate_limit_per_second' => '-1B', 'health-check-type' => process.health_check_type, }, { @@ -210,6 +215,7 @@ 'instances' => worker_process.instances, 'memory' => "#{worker_process.memory}M", 'disk_quota' => "#{worker_process.disk_quota}M", + 'log_rate_limit_per_second' => '1M', 'command' => worker_process.command, 'health-check-type' => worker_process.health_check_type, 'health-check-http-endpoint' => worker_process.health_check_http_endpoint, diff --git a/spec/request/apps_spec.rb b/spec/request/apps_spec.rb index 831bb07450c..41e5a08f589 100644 --- a/spec/request/apps_spec.rb +++ b/spec/request/apps_spec.rb @@ -2378,6 +2378,152 @@ it_behaves_like 'permissions for single object endpoint', ALL_PERMISSIONS end + + describe 'limiting the application log rates' do + let(:log_rate_limit) { -1 } + let(:space_log_rate_limit) { -1 } + let(:org_log_rate_limit) { -1 } + let(:org_quota_definition) { VCAP::CloudController::QuotaDefinition.make(log_rate_limit: org_log_rate_limit) } + let(:org) { VCAP::CloudController::Organization.make(quota_definition: org_quota_definition) } + let(:space_quota_definition) { VCAP::CloudController::SpaceQuotaDefinition.make(organization: org, log_rate_limit: space_log_rate_limit) } + let(:space) { VCAP::CloudController::Space.make(organization: org, space_quota_definition: space_quota_definition) } + let!(:process_model) { VCAP::CloudController::ProcessModel.make(app: app_model, log_rate_limit: log_rate_limit) } + let(:app_model) { + VCAP::CloudController::AppModel.make( + :buildpack, + name: 'app-name', + space: space, + desired_state: 'STOPPED', + ) + } + let(:droplet) { VCAP::CloudController::DropletModel.make(app: app_model, process_types: { web: 'webby' }) } + + before do + app_model.update(droplet_guid: droplet.guid) + end + + describe 'space quotas' do + context 'when both the space and the app do not specify a log rate limit' do + let(:log_rate_limit) { -1 } + let(:space_log_rate_limit) { -1 } + + it 'starts the app successfully' do + post "/v3/apps/#{app_model.guid}/actions/start", nil, admin_header + + expect(last_response.status).to eq(200) + end + end + + context "when the app fits in the space's log rate limit" do + let(:log_rate_limit) { 199 } + let(:space_log_rate_limit) { 200 } + + it 'starts the app successfully' do + post "/v3/apps/#{app_model.guid}/actions/start", nil, admin_header + + expect(last_response.status).to eq(200) + end + end + + context "when the app's log rate limit is unspecified, but the space specifies a log rate limit" do + let(:log_rate_limit) { -1 } + let(:space_log_rate_limit) { 200 } + + it 'fails to start the app' do + post "/v3/apps/#{app_model.guid}/actions/start", nil, admin_header + + expect(last_response.status).to eq(422) + expect(last_response).to have_error_message("log_rate_limit cannot be unlimited in space '#{space.name}'.") + end + end + + context "when the app's log rate limit is larger than the limit specified by the space" do + let(:log_rate_limit) { 201 } + let(:space_log_rate_limit) { 200 } + + it 'fails to start the app' do + post "/v3/apps/#{app_model.guid}/actions/start", nil, admin_header + + expect(last_response.status).to eq(422) + expect(last_response).to have_error_message('log_rate_limit exceeds space log rate quota') + end + end + + context "when the space's quota is more strict that the org's quota, the space quota controls" do + let(:log_rate_limit) { 201 } + let(:space_log_rate_limit) { 200 } + let(:org_log_rate_limit) { 201 } + + it 'fails to start the app' do + post "/v3/apps/#{app_model.guid}/actions/start", nil, admin_header + + expect(last_response.status).to eq(422) + expect(last_response).to have_error_message('log_rate_limit exceeds space log rate quota') + end + end + end + + describe 'organization quotas' do + context 'when both the org and the app do not specify a log rate limit' do + let(:log_rate_limit) { -1 } + let(:org_log_rate_limit) { -1 } + + it 'starts the app successfully' do + post "/v3/apps/#{app_model.guid}/actions/start", nil, admin_header + + expect(last_response.status).to eq(200) + end + end + + context "when the app fits in the org's log rate limit" do + let(:log_rate_limit) { 199 } + let(:org_log_rate_limit) { 200 } + + it 'starts the app successfully' do + post "/v3/apps/#{app_model.guid}/actions/start", nil, admin_header + + expect(last_response.status).to eq(200) + end + end + + context "when the app's log rate limit is unspecified, but the org specifies a log rate limit" do + let(:log_rate_limit) { -1 } + let(:org_log_rate_limit) { 200 } + + it 'fails to start the app' do + post "/v3/apps/#{app_model.guid}/actions/start", nil, admin_header + + expect(last_response.status).to eq(422) + expect(last_response).to have_error_message("log_rate_limit cannot be unlimited in organization '#{org.name}'.") + end + end + + context "when the app's log rate limit is larger than the limit specified by the org" do + let(:log_rate_limit) { 201 } + let(:org_log_rate_limit) { 200 } + + it 'fails to start the app' do + post "/v3/apps/#{app_model.guid}/actions/start", nil, admin_header + + expect(last_response.status).to eq(422) + expect(last_response).to have_error_message('log_rate_limit exceeds organization log rate quota') + end + end + + context "when the org's quota is more strict that the space's quota, the org quota controls" do + let(:log_rate_limit) { 201 } + let(:space_log_rate_limit) { 202 } + let(:org_log_rate_limit) { 200 } + + it 'fails to start the app' do + post "/v3/apps/#{app_model.guid}/actions/start", nil, admin_header + + expect(last_response.status).to eq(422) + expect(last_response).to have_error_message('log_rate_limit exceeds organization log rate quota') + end + end + end + end end context 'events' do diff --git a/spec/request/organization_quotas_spec.rb b/spec/request/organization_quotas_spec.rb index 6fcbec4699a..e3dcf90c3f2 100644 --- a/spec/request/organization_quotas_spec.rb +++ b/spec/request/organization_quotas_spec.rb @@ -35,7 +35,8 @@ module VCAP::CloudController total_memory_in_mb: nil, per_process_memory_in_mb: nil, total_instances: nil, - per_app_tasks: nil + per_app_tasks: nil, + log_rate_limit_in_bytes_per_second: nil }, services: { paid_services_allowed: true, @@ -91,7 +92,8 @@ module VCAP::CloudController total_memory_in_mb: 5120, per_process_memory_in_mb: 1024, total_instances: 10, - per_app_tasks: 5 + per_app_tasks: 5, + log_rate_limit_in_bytes_per_second: 2000 }, services: { paid_services_allowed: false, @@ -118,7 +120,8 @@ module VCAP::CloudController total_memory_in_mb: 5120, per_process_memory_in_mb: 1024, total_instances: 10, - per_app_tasks: 5 + per_app_tasks: 5, + log_rate_limit_in_bytes_per_second: 2000 }, services: { paid_services_allowed: false, @@ -319,7 +322,8 @@ module VCAP::CloudController total_memory_in_mb: 5120, per_process_memory_in_mb: 1024, total_instances: nil, - per_app_tasks: 5 + per_app_tasks: 5, + log_rate_limit_in_bytes_per_second: 2000 }, services: { paid_services_allowed: false, @@ -346,7 +350,8 @@ module VCAP::CloudController total_memory_in_mb: 5120, per_process_memory_in_mb: 1024, total_instances: nil, - per_app_tasks: 5 + per_app_tasks: 5, + log_rate_limit_in_bytes_per_second: 2000 }, services: { paid_services_allowed: false, @@ -423,6 +428,7 @@ module VCAP::CloudController expect(org_quota_to_update.reload.app_task_limit).to eq(9) expect(org_quota_to_update.reload.memory_limit).to eq(-1) expect(org_quota_to_update.reload.total_services).to eq(14) + expect(org_quota_to_update.reload.log_rate_limit).to eq(-1) expect(org_quota_to_update.reload.non_basic_services_allowed).to be_falsey end @@ -434,6 +440,7 @@ module VCAP::CloudController expect(org_quota_to_update.reload.app_task_limit).to eq(9) expect(org_quota_to_update.reload.memory_limit).to eq(-1) expect(org_quota_to_update.reload.total_services).to eq(14) + expect(org_quota_to_update.reload.log_rate_limit).to eq(-1) expect(org_quota_to_update.reload.non_basic_services_allowed).to be_falsey end end @@ -591,7 +598,8 @@ def generate_org_quota_single_response(list_of_orgs) total_memory_in_mb: 20480, per_process_memory_in_mb: nil, total_instances: nil, - per_app_tasks: nil + per_app_tasks: nil, + log_rate_limit_in_bytes_per_second: nil }, services: { paid_services_allowed: true, @@ -639,7 +647,8 @@ def generate_default_org_quota_response(global_read) total_memory_in_mb: 10240, per_process_memory_in_mb: nil, total_instances: nil, - per_app_tasks: nil + per_app_tasks: nil, + log_rate_limit_in_bytes_per_second: nil }, services: { paid_services_allowed: true, diff --git a/spec/request/processes_spec.rb b/spec/request/processes_spec.rb index 4ad1144cf99..7a766fce183 100644 --- a/spec/request/processes_spec.rb +++ b/spec/request/processes_spec.rb @@ -37,6 +37,7 @@ instances: 2, memory: 1024, disk_quota: 1024, + log_rate_limit: 1_048_576, command: 'rackup', ) } @@ -48,6 +49,7 @@ instances: 1, memory: 100, disk_quota: 200, + log_rate_limit: 400, command: 'start worker', ) } @@ -111,11 +113,12 @@ } }, }, - 'type' => 'web', - 'command' => '[PRIVATE DATA HIDDEN IN LISTS]', - 'instances' => 2, - 'memory_in_mb' => 1024, - 'disk_in_mb' => 1024, + 'type' => 'web', + 'command' => '[PRIVATE DATA HIDDEN IN LISTS]', + 'instances' => 2, + 'memory_in_mb' => 1024, + 'disk_in_mb' => 1024, + 'log_rate_limit_in_bytes_per_second' => 1_048_576, 'health_check' => { 'type' => 'port', 'data' => { @@ -140,11 +143,12 @@ 'app' => { 'data' => { 'guid' => app_model.guid } }, 'revision' => nil, }, - 'type' => 'worker', - 'command' => '[PRIVATE DATA HIDDEN IN LISTS]', - 'instances' => 1, - 'memory_in_mb' => 100, - 'disk_in_mb' => 200, + 'type' => 'worker', + 'command' => '[PRIVATE DATA HIDDEN IN LISTS]', + 'instances' => 1, + 'memory_in_mb' => 100, + 'disk_in_mb' => 200, + 'log_rate_limit_in_bytes_per_second' => 400, 'health_check' => { 'type' => 'port', 'data' => { @@ -229,6 +233,7 @@ instances: 2, memory: 1024, disk_quota: 1024, + log_rate_limit: 1_048_576, command: 'rackup', ) } @@ -270,6 +275,7 @@ instances: 2, memory: 1024, disk_quota: 1024, + log_rate_limit: 1_048_576, command: 'rackup', ) } @@ -307,6 +313,7 @@ instances: 3, memory: 2048, disk_quota: 2048, + log_rate_limit: 2_097_152, command: 'at ease' ) end @@ -383,6 +390,7 @@ instances: 2, memory: 1024, disk_quota: 1024, + log_rate_limit: 1_048_576, command: 'rackup', ) end @@ -394,10 +402,11 @@ 'app' => { 'data' => { 'guid' => app_model.guid } }, 'revision' => { 'data' => { 'guid' => revision.guid } }, }, - 'command' => 'rackup', - 'instances' => 2, - 'memory_in_mb' => 1024, - 'disk_in_mb' => 1024, + 'command' => 'rackup', + 'instances' => 2, + 'memory_in_mb' => 1024, + 'disk_in_mb' => 1024, + 'log_rate_limit_in_bytes_per_second' => 1_048_576, 'health_check' => { 'type' => 'port', 'data' => { @@ -494,12 +503,14 @@ uptime: 12345, mem_quota: process[:memory] * 1024 * 1024, disk_quota: process[:disk_quota] * 1024 * 1024, + log_rate_limit: process[:log_rate_limit], fds_quota: process.file_descriptors, usage: { time: usage_time, cpu: 80, mem: 128, disk: 1024, + log_rate: 1024, } } }, @@ -522,6 +533,7 @@ 'cpu' => 80, 'mem' => 128, 'disk' => 1024, + 'log_rate' => 1024, }, 'host' => 'toast', 'instance_ports' => [ @@ -541,7 +553,8 @@ 'uptime' => 12345, 'mem_quota' => 1073741824, 'disk_quota' => 1073741824, - 'fds_quota' => 16384 + 'fds_quota' => 16384, + 'log_rate_limit' => 1_048_576 }] } end @@ -602,6 +615,7 @@ instances: 2, memory: 1024, disk_quota: 1024, + log_rate_limit: 1_048_576, command: 'rackup', ports: [4444, 5555], health_check_type: 'port', @@ -634,6 +648,7 @@ 'instances' => 2, 'memory_in_mb' => 1024, 'disk_in_mb' => 1024, + 'log_rate_limit_in_bytes_per_second' => 1_048_576, 'health_check' => { 'type' => 'process', 'data' => { @@ -748,15 +763,17 @@ instances: 2, memory: 1024, disk_quota: 1024, + log_rate_limit: 1_048_576, command: 'rackup', ) end let(:scale_request) do { - instances: 5, + instances: 5, memory_in_mb: 10, - disk_in_mb: 20, + disk_in_mb: 20, + log_rate_limit_in_bytes_per_second: 40, } end @@ -773,6 +790,7 @@ 'instances' => 5, 'memory_in_mb' => 10, 'disk_in_mb' => 20, + 'log_rate_limit_in_bytes_per_second' => 40, 'health_check' => { 'type' => 'port', 'data' => { @@ -805,6 +823,7 @@ expect(process.instances).to eq(5) expect(process.memory).to eq(10) expect(process.disk_quota).to eq(20) + expect(process.log_rate_limit).to eq(40) events = VCAP::CloudController::Event.where(actor: developer.guid).all @@ -826,7 +845,8 @@ 'request' => { 'instances' => 5, 'memory_in_mb' => 10, - 'disk_in_mb' => 20 + 'disk_in_mb' => 20, + 'log_rate_limit_in_bytes_per_second' => 40, } }) end @@ -875,6 +895,7 @@ instances: 5, memory_in_mb: 10, disk_in_mb: 20, + log_rate_limit_in_bytes_per_second: 40, } post "/v3/processes/#{process.guid}/actions/scale", scale_request.to_json, headers_for(space_supporter) @@ -886,6 +907,7 @@ expect(process.instances).to eq(5) expect(process.memory).to eq(10) expect(process.disk_quota).to eq(20) + expect(process.log_rate_limit).to eq(40) end end @@ -924,6 +946,20 @@ expect(process.memory).to eq(1024) end + it 'returns a helpful error when the log quota is too small' do + scale_request = { + log_rate_limit_in_bytes_per_second: -2, + } + + post "/v3/processes/#{process.guid}/actions/scale", scale_request.to_json, developer_headers + + expect(last_response.status).to eq(422) + expect(parsed_response['errors'][0]['detail']).to eq 'Log rate limit in bytes per second must be greater than or equal to -1' + + process.reload + expect(process.log_rate_limit).to eq(1_048_576) + end + context 'telemetry' do let(:process) { VCAP::CloudController::ProcessModel.make( @@ -933,6 +969,7 @@ instances: 2, memory: 1024, disk_quota: 1024, + log_rate_limit: 1_048_576, command: 'rackup', ) } @@ -941,6 +978,7 @@ instances: 5, memory_in_mb: 10, disk_in_mb: 20, + log_rate_limit_in_bytes_per_second: 40, } end @@ -954,6 +992,7 @@ 'instance-count' => 5, 'memory-in-mb' => 10, 'disk-in-mb' => 20, + 'log-rate-in-bytes-per-second' => 40, 'process-type' => 'web', 'app-id' => Digest::SHA256.hexdigest(process.app.guid), 'user-id' => Digest::SHA256.hexdigest(developer.guid), @@ -1042,6 +1081,7 @@ instances: 2, memory: 1024, disk_quota: 1024, + log_rate_limit: 1_048_576, command: 'rackup', ) } @@ -1055,6 +1095,7 @@ instances: 1, memory: 100, disk_quota: 200, + log_rate_limit: 400, command: 'start worker', ) } @@ -1103,6 +1144,7 @@ 'instances' => 2, 'memory_in_mb' => 1024, 'disk_in_mb' => 1024, + 'log_rate_limit_in_bytes_per_second' => 1_048_576, 'health_check' => { 'type' => 'port', 'data' => { @@ -1136,6 +1178,7 @@ 'instances' => 1, 'memory_in_mb' => 100, 'disk_in_mb' => 200, + 'log_rate_limit_in_bytes_per_second' => 400, 'health_check' => { 'type' => 'port', 'data' => { @@ -1237,6 +1280,7 @@ instances: 2, memory: 1024, disk_quota: 1024, + log_rate_limit: 1_048_576, command: 'rackup', ) } @@ -1253,6 +1297,7 @@ 'instances' => 2, 'memory_in_mb' => 1024, 'disk_in_mb' => 1024, + 'log_rate_limit_in_bytes_per_second' => 1_048_576, 'health_check' => { 'type' => 'port', 'data' => { @@ -1322,6 +1367,7 @@ instances: 2, memory: 1024, disk_quota: 1024, + log_rate_limit: 1_048_576, command: 'rackup', ports: [4444, 5555], health_check_type: 'port', @@ -1353,6 +1399,7 @@ 'instances' => 2, 'memory_in_mb' => 1024, 'disk_in_mb' => 1024, + 'log_rate_limit_in_bytes_per_second' => 1_048_576, 'health_check' => { 'type' => 'http', 'data' => { @@ -1435,7 +1482,9 @@ instances: 2, memory: 1024, disk_quota: 1024, + log_rate_limit: 1_048_576, command: 'rackup', + state: 'STARTED', ) end @@ -1443,6 +1492,7 @@ instances: 5, memory_in_mb: 10, disk_in_mb: 20, + log_rate_limit_in_bytes_per_second: 40, } end @@ -1459,6 +1509,7 @@ 'instances' => 5, 'memory_in_mb' => 10, 'disk_in_mb' => 20, + 'log_rate_limit_in_bytes_per_second' => 40, 'health_check' => { 'type' => 'port', 'data' => { @@ -1490,6 +1541,7 @@ expect(process.instances).to eq(5) expect(process.memory).to eq(10) expect(process.disk_quota).to eq(20) + expect(process.log_rate_limit).to eq(40) events = VCAP::CloudController::Event.where(actor: developer.guid).all @@ -1511,11 +1563,34 @@ 'request' => { 'instances' => 5, 'memory_in_mb' => 10, - 'disk_in_mb' => 20 + 'disk_in_mb' => 20, + 'log_rate_limit_in_bytes_per_second' => 40, } }) end + context 'when the log rate limit would be exceeded by adding additional instances' do + let(:scale_request) do + { + instances: 5 + } + end + + before do + org.update(quota_definition: VCAP::CloudController::QuotaDefinition.make(log_rate_limit: 2_097_152)) + end + + it 'fails to scale the process' do + post "/v3/apps/#{app_model.guid}/processes/web/actions/scale", scale_request.to_json, developer_headers + + expect(last_response.status).to eq(422) + expect(parsed_response['errors'][0]['detail']).to eq 'log_rate_limit exceeds organization log rate quota' + + process.reload + expect(process.instances).to eq(2) + end + end + context 'when the user is assigned the space_supporter role' do let(:space_supporter) do user = VCAP::CloudController::User.make @@ -1534,6 +1609,7 @@ expect(process.instances).to eq(5) expect(process.memory).to eq(10) expect(process.disk_quota).to eq(20) + expect(process.log_rate_limit).to eq(40) end end @@ -1548,6 +1624,7 @@ 'instance-count' => 5, 'memory-in-mb' => 10, 'disk-in-mb' => 20, + 'log-rate-in-bytes-per-second' => 40, 'process-type' => 'web', 'app-id' => Digest::SHA256.hexdigest(app_model.guid), 'user-id' => Digest::SHA256.hexdigest(developer.guid), diff --git a/spec/request/space_manifests_spec.rb b/spec/request/space_manifests_spec.rb index 3c2c4f6da9c..529eac982b1 100644 --- a/spec/request/space_manifests_spec.rb +++ b/spec/request/space_manifests_spec.rb @@ -27,6 +27,7 @@ 'instances' => 4, 'memory' => '2048MB', 'disk_quota' => '1.5GB', + 'log_rate_limit_per_second' => '1MB', 'buildpack' => buildpack.name, 'stack' => buildpack.stack, 'command' => 'new-command', @@ -73,6 +74,7 @@ 'instances' => 3, 'memory' => '2048MB', 'disk_quota' => '1.5GB', + 'log_rate_limit_per_second' => '100KB', 'buildpack' => buildpack.name, 'stack' => buildpack.stack, 'command' => 'newer-command', @@ -172,6 +174,7 @@ expect(web_process.instances).to eq(4) expect(web_process.memory).to eq(2048) expect(web_process.disk_quota).to eq(1536) + expect(web_process.log_rate_limit).to eq(1_048_576) expect(web_process.command).to eq('new-command') expect(web_process.health_check_type).to eq('http') expect(web_process.health_check_http_endpoint).to eq('/health') @@ -244,6 +247,7 @@ 'instances' => 4, 'memory' => '2048MB', 'disk_quota' => '1.5GB', + 'log_rate_limit_per_second' => '1GB', 'buildpack' => buildpack.name, 'stack' => buildpack.stack, 'command' => 'new-command', @@ -267,6 +271,7 @@ 'instances' => 4, 'memory' => '2048MB', 'disk_quota' => '1.5GB', + 'log_rate_limit_per_second' => '-1B', 'buildpack' => buildpack.name, 'stack' => buildpack.stack, 'command' => 'new-command', @@ -304,6 +309,7 @@ expect(web_process.instances).to eq(4) expect(web_process.memory).to eq(2048) expect(web_process.disk_quota).to eq(1536) + expect(web_process.log_rate_limit).to eq(-1) expect(web_process.command).to eq('new-command') expect(web_process.health_check_type).to eq('http') expect(web_process.health_check_http_endpoint).to eq('/health') @@ -455,6 +461,56 @@ end end + context 'when -1 is given as a log rate limit' do + let(:yml_manifest) do + { + 'version' => 1, + 'applications' => [ + { 'name' => app1_model.name, + 'log_rate_limit_per_second' => -1 + }, + ] + }.to_yaml + end + + it 'interprets the log rate limit as unlimited' do + post "/v3/spaces/#{space.guid}/actions/apply_manifest", yml_manifest, yml_headers(user_header) + + expect(last_response.status).to eq(202) + job_guid = VCAP::CloudController::PollableJobModel.last.guid + expect(last_response.headers['Location']).to match(%r(/v3/jobs/#{job_guid})) + + Delayed::Worker.new.work_off + expect(VCAP::CloudController::PollableJobModel.find(guid: job_guid)).to be_complete, + VCAP::CloudController::PollableJobModel.find(guid: job_guid).cf_api_error + + app1_model.reload + expect(app1_model.processes.first.log_rate_limit).to eq(-1) + end + end + + context 'when applying the manifest to an app which is already exceeding the log rate limit' do + before do + app1_model.web_processes.first.update(state: VCAP::CloudController::ProcessModel::STARTED, instances: 4) + space.update(space_quota_definition: + VCAP::CloudController::SpaceQuotaDefinition.make(organization: space.organization, log_rate_limit: 0)) + end + + it 'successfully applies the manifest' do + post "/v3/spaces/#{space.guid}/actions/apply_manifest", yml_manifest, yml_headers(user_header) + + expect(last_response.status).to eq(202) + + job_guid = VCAP::CloudController::PollableJobModel.last.guid + expect(last_response.headers['Location']).to match(%r(/v3/jobs/#{job_guid})) + + Delayed::Worker.new.work_off + # job does not restart app, so applying the manifest succeeds + expect(VCAP::CloudController::PollableJobModel.find(guid: job_guid)).to be_complete, + VCAP::CloudController::PollableJobModel.find(guid: job_guid).cf_api_error + end + end + describe 'audit events' do let!(:process1) { nil } @@ -465,6 +521,7 @@ 'instances' => 4, 'memory' => '2048MB', 'disk_quota' => '1.5GB', + 'log_rate_limit_per_second' => '300B', 'buildpack' => buildpack.name, 'stack' => buildpack.stack, 'command' => 'new-command', diff --git a/spec/request/space_quotas_spec.rb b/spec/request/space_quotas_spec.rb index 5c67463dc72..67a3dfed230 100644 --- a/spec/request/space_quotas_spec.rb +++ b/spec/request/space_quotas_spec.rb @@ -88,7 +88,8 @@ module VCAP::CloudController total_memory_in_mb: 5120, per_process_memory_in_mb: 1024, total_instances: nil, - per_app_tasks: 5 + per_app_tasks: 5, + log_rate_limit_in_bytes_per_second: 2000 }, services: { paid_services_allowed: false, @@ -112,7 +113,8 @@ module VCAP::CloudController total_memory_in_mb: 5120, per_process_memory_in_mb: 1024, total_instances: nil, - per_app_tasks: 5 + per_app_tasks: 5, + log_rate_limit_in_bytes_per_second: 2000 }, services: { paid_services_allowed: false, @@ -208,6 +210,7 @@ module VCAP::CloudController expect(last_response).to have_status_code(200) expect(space_quota_to_update.reload.app_task_limit).to eq(9) expect(space_quota_to_update.reload.memory_limit).to eq(-1) + expect(space_quota_to_update.reload.log_rate_limit).to eq(-1) expect(space_quota_to_update.reload.total_services).to eq(14) expect(space_quota_to_update.reload.non_basic_services_allowed).to be_falsey end @@ -219,6 +222,7 @@ module VCAP::CloudController expect(last_response).to have_status_code(200) expect(space_quota_to_update.reload.app_task_limit).to eq(9) expect(space_quota_to_update.reload.memory_limit).to eq(-1) + expect(space_quota_to_update.reload.log_rate_limit).to eq(-1) expect(space_quota_to_update.reload.total_services).to eq(14) expect(space_quota_to_update.reload.non_basic_services_allowed).to be_falsey end @@ -406,7 +410,8 @@ module VCAP::CloudController total_memory_in_mb: nil, per_process_memory_in_mb: nil, total_instances: nil, - per_app_tasks: nil + per_app_tasks: nil, + log_rate_limit_in_bytes_per_second: nil }, services: { paid_services_allowed: true, @@ -495,7 +500,8 @@ module VCAP::CloudController total_memory_in_mb: nil, per_process_memory_in_mb: nil, total_instances: nil, - per_app_tasks: nil + per_app_tasks: nil, + log_rate_limit_in_bytes_per_second: nil }, services: { paid_services_allowed: true, @@ -555,7 +561,8 @@ module VCAP::CloudController total_memory_in_mb: 5120, per_process_memory_in_mb: 1024, total_instances: 10, - per_app_tasks: 5 + per_app_tasks: 5, + log_rate_limit_in_bytes_per_second: 3000 }, services: { paid_services_allowed: false, @@ -589,7 +596,8 @@ module VCAP::CloudController total_memory_in_mb: 5120, per_process_memory_in_mb: 1024, total_instances: 10, - per_app_tasks: 5 + per_app_tasks: 5, + log_rate_limit_in_bytes_per_second: 3000 }, services: { paid_services_allowed: false, @@ -968,7 +976,8 @@ def make_space_quota_json(space_quota, associated_spaces=space_quota.spaces) total_memory_in_mb: 20480, per_process_memory_in_mb: nil, total_instances: nil, - per_app_tasks: 5 + per_app_tasks: 5, + log_rate_limit_in_bytes_per_second: nil }, services: { paid_services_allowed: true, diff --git a/spec/request/tasks_spec.rb b/spec/request/tasks_spec.rb index 0b030affc90..c2946239d48 100644 --- a/spec/request/tasks_spec.rb +++ b/spec/request/tasks_spec.rb @@ -2,9 +2,14 @@ require 'request_spec_shared_examples' RSpec.describe 'Tasks' do + let(:org_quota_definition) { VCAP::CloudController::QuotaDefinition.make(log_rate_limit: org_log_rate_limit) } + let(:space_quota_definition) { VCAP::CloudController::SpaceQuotaDefinition.make(organization: org, log_rate_limit: space_log_rate_limit) } + let(:space_log_rate_limit) { -1 } + let(:org_log_rate_limit) { -1 } + let(:task_log_rate_limit_in_bytes_per_second) { -1 } let(:user) { VCAP::CloudController::User.make } - let(:org) { space.organization } - let(:space) { VCAP::CloudController::Space.make } + let(:org) { VCAP::CloudController::Organization.make(quota_definition: org_quota_definition) } + let(:space) { VCAP::CloudController::Space.make(space_quota_definition: space_quota_definition, organization: org) } let(:app_model) { VCAP::CloudController::AppModel.make(space_guid: space.guid) } let(:droplet) do VCAP::CloudController::DropletModel.make( @@ -101,6 +106,7 @@ droplet: app_model.droplet, memory_in_mb: 5, disk_in_mb: 10, + log_rate_limit: 20, ) task2 = VCAP::CloudController::TaskModel.make( name: 'task two', @@ -109,6 +115,7 @@ droplet: app_model.droplet, memory_in_mb: 100, disk_in_mb: 500, + log_rate_limit: 1024, ) VCAP::CloudController::TaskModel.make( app_guid: app_model.guid, @@ -137,7 +144,8 @@ 'state' => 'RUNNING', 'memory_in_mb' => 5, 'disk_in_mb' => 10, - 'result' => { + 'log_rate_limit_in_bytes_per_second' => 20, + 'result' => { 'failure_reason' => nil }, 'droplet_guid' => task1.droplet.guid, @@ -168,7 +176,8 @@ 'state' => 'RUNNING', 'memory_in_mb' => 100, 'disk_in_mb' => 500, - 'result' => { + 'log_rate_limit_in_bytes_per_second' => 1024, + 'result' => { 'failure_reason' => nil }, 'droplet_guid' => task2.droplet.guid, @@ -216,6 +225,7 @@ droplet: app_model.droplet, memory_in_mb: 5, disk_in_mb: 10, + log_rate_limit: 20, ) end @@ -228,7 +238,8 @@ 'state' => 'RUNNING', 'memory_in_mb' => 256, 'disk_in_mb' => nil, - 'result' => { + 'log_rate_limit_in_bytes_per_second' => -1, + 'result' => { 'failure_reason' => nil }, 'droplet_guid' => task1.droplet.guid, @@ -259,7 +270,8 @@ 'state' => 'RUNNING', 'memory_in_mb' => 5, 'disk_in_mb' => 10, - 'result' => { + 'log_rate_limit_in_bytes_per_second' => 20, + 'result' => { 'failure_reason' => nil }, 'droplet_guid' => task2.droplet.guid, @@ -407,6 +419,7 @@ droplet: app_model.droplet, memory_in_mb: 5, disk_in_mb: 50, + log_rate_limit: 64, ) end let(:api_call) { lambda { |user_headers| get "/v3/tasks/#{task.guid}", nil, user_headers } } @@ -418,7 +431,8 @@ 'state' => 'RUNNING', 'memory_in_mb' => 5, 'disk_in_mb' => 50, - 'result' => { + 'log_rate_limit_in_bytes_per_second' => 64, + 'result' => { 'failure_reason' => nil }, 'droplet_guid' => task.droplet.guid, @@ -498,8 +512,10 @@ command: 'echo task', app_guid: app_model.guid, droplet_guid: app_model.droplet.guid, - disk_in_mb: 50, - memory_in_mb: 5) + disk_in_mb: 50, + memory_in_mb: 5, + log_rate_limit: 10, + ) } let(:request_body) do { @@ -528,7 +544,8 @@ 'state' => 'RUNNING', 'memory_in_mb' => 5, 'disk_in_mb' => 50, - 'result' => { + 'log_rate_limit_in_bytes_per_second' => 10, + 'result' => { 'failure_reason' => nil }, 'droplet_guid' => task.droplet.guid, @@ -605,6 +622,7 @@ state: 'CANCELING', memory_in_mb: 256, disk_in_mb: nil, + log_rate_limit_in_bytes_per_second: -1, result: { failure_reason: nil }, @@ -683,6 +701,7 @@ droplet: app_model.droplet, memory_in_mb: 5, disk_in_mb: 50, + log_rate_limit: 64, ) task2 = VCAP::CloudController::TaskModel.make( name: 'task two', @@ -691,6 +710,7 @@ droplet: app_model.droplet, memory_in_mb: 100, disk_in_mb: 500, + log_rate_limit: 256, ) VCAP::CloudController::TaskModel.make( app_guid: app_model.guid, @@ -718,7 +738,8 @@ 'state' => 'RUNNING', 'memory_in_mb' => 5, 'disk_in_mb' => 50, - 'result' => { + 'log_rate_limit_in_bytes_per_second' => 64, + 'result' => { 'failure_reason' => nil }, 'droplet_guid' => task1.droplet.guid, @@ -750,7 +771,8 @@ 'state' => 'RUNNING', 'memory_in_mb' => 100, 'disk_in_mb' => 500, - 'result' => { + 'log_rate_limit_in_bytes_per_second' => 256, + 'result' => { 'failure_reason' => nil }, 'droplet_guid' => task2.droplet.guid, @@ -815,7 +837,8 @@ 'state' => 'RUNNING', 'memory_in_mb' => 256, 'disk_in_mb' => nil, - 'result' => { + 'log_rate_limit_in_bytes_per_second' => -1, + 'result' => { 'failure_reason' => nil }, 'droplet_guid' => task1.droplet.guid, @@ -846,7 +869,8 @@ 'state' => 'RUNNING', 'memory_in_mb' => 5, 'disk_in_mb' => 10, - 'result' => { + 'log_rate_limit_in_bytes_per_second' => -1, + 'result' => { 'failure_reason' => nil }, 'droplet_guid' => task2.droplet.guid, @@ -997,6 +1021,7 @@ command: 'be rake && true', memory_in_mb: 1234, disk_in_mb: 1000, + log_rate_limit_in_bytes_per_second: task_log_rate_limit_in_bytes_per_second, metadata: { labels: { bananas: 'gros_michel', @@ -1029,7 +1054,8 @@ 'state' => 'RUNNING', 'memory_in_mb' => 1234, 'disk_in_mb' => 1000, - 'result' => { + 'log_rate_limit_in_bytes_per_second' => task_log_rate_limit_in_bytes_per_second, + 'result' => { 'failure_reason' => nil }, 'droplet_guid' => droplet.guid, @@ -1082,6 +1108,71 @@ }) end + describe 'log_rate_limit' do + context 'when the request does not specify a log rate limit' do + before do + TestConfig.config[:default_app_log_rate_limit_in_bytes_per_second] = 9876 + end + + it 'the default is applied' do + post "/v3/apps/#{app_model.guid}/tasks", body.except(:log_rate_limit_in_bytes_per_second).to_json, developer_headers + expect(last_response.status).to eq(202) + expect(VCAP::CloudController::TaskModel.last.log_rate_limit).to eq(9876) + end + end + + context 'when there are org or space log rate limits' do + let(:space_log_rate_limit) { 200 } + let(:org_log_rate_limit) { 201 } + + context 'when the task specifies a rate limit that fits in the quota' do + let(:task_log_rate_limit_in_bytes_per_second) { 199 } + + it 'succeeds' do + post "/v3/apps/#{app_model.guid}/tasks", body.to_json, developer_headers + expect(last_response.status).to eq(202) + end + end + + context 'when the task specifies unlimited rate limit' do + let(:task_log_rate_limit_in_bytes_per_second) { -1 } + + it 'returns an error' do + post "/v3/apps/#{app_model.guid}/tasks", body.to_json, developer_headers + expect(last_response.status).to eq(422) + expect(last_response).to have_error_message("log_rate_limit cannot be unlimited in organization '#{org.name}'.") + expect(last_response).to have_error_message("log_rate_limit cannot be unlimited in space '#{space.name}'.") + end + end + + context 'when the task specifies a rate limit that does not fit in the quota' do + let(:task_log_rate_limit_in_bytes_per_second) { 202 } + + context 'fails to fit in space quota' do + let(:space_log_rate_limit) { 200 } + let(:org_log_rate_limit) { -1 } + + it 'returns an error' do + post "/v3/apps/#{app_model.guid}/tasks", body.to_json, developer_headers + expect(last_response.status).to eq(422) + expect(last_response).to have_error_message('log_rate_limit exceeds space log rate quota') + end + end + + context 'fails to fit in org quota' do + let(:space_log_rate_limit) { -1 } + let(:org_log_rate_limit) { 200 } + + it 'returns an error' do + post "/v3/apps/#{app_model.guid}/tasks", body.to_json, developer_headers + expect(last_response.status).to eq(422) + expect(last_response).to have_error_message('log_rate_limit exceeds organization log rate quota') + end + end + end + end + end + context 'when requesting a specific droplet' do let(:non_assigned_droplet) do VCAP::CloudController::DropletModel.make( @@ -1137,7 +1228,8 @@ 'state' => 'RUNNING', 'memory_in_mb' => 1234, 'disk_in_mb' => 1000, - 'result' => { + 'log_rate_limit_in_bytes_per_second' => process.log_rate_limit, + 'result' => { 'failure_reason' => nil }, 'droplet_guid' => droplet.guid, diff --git a/spec/request/v2/apps_spec.rb b/spec/request/v2/apps_spec.rb index 4233bd67008..85a0fc26271 100644 --- a/spec/request/v2/apps_spec.rb +++ b/spec/request/v2/apps_spec.rb @@ -61,6 +61,7 @@ 'memory' => 1024, 'instances' => 1, 'disk_quota' => 1024, + 'log_rate_limit' => 1_048_576, 'state' => 'STOPPED', 'version' => process.version, 'command' => 'hello_world', @@ -228,6 +229,7 @@ 'memory' => 1024, 'instances' => 1, 'disk_quota' => 1024, + 'log_rate_limit' => 1_048_576, 'state' => 'STOPPED', 'version' => process.version, 'command' => 'hello_world', @@ -447,6 +449,7 @@ 'memory' => 1024, 'instances' => 1, 'disk_quota' => 1024, + 'log_rate_limit' => 1_048_576, 'state' => 'STOPPED', 'version' => process.version, 'command' => 'app-command', @@ -522,6 +525,7 @@ 'memory' => 1024, 'instances' => 1, 'disk_quota' => 1024, + 'log_rate_limit' => 1_048_576, 'state' => 'STOPPED', 'version' => process.version, 'command' => nil, @@ -667,6 +671,7 @@ 'memory' => 1024, 'instances' => 1, 'disk_quota' => 1024, + 'log_rate_limit' => 1_048_576, 'state' => 'STOPPED', 'version' => process.version, 'command' => nil, @@ -746,6 +751,7 @@ }, 'memory' => 1024, 'instances' => 1, + 'log_rate_limit' => 1_048_576, 'disk_quota' => 1024, 'state' => 'STARTED', 'version' => process.version, @@ -1012,6 +1018,7 @@ 'memory' => 1024, 'instances' => 1, 'disk_quota' => 1024, + 'log_rate_limit' => 1_048_576, 'state' => 'STARTED', 'version' => process.version, 'command' => nil, @@ -1089,6 +1096,7 @@ 'memory' => 1024, 'instances' => 1, 'disk_quota' => 1024, + 'log_rate_limit' => 1_048_576, 'state' => 'STARTED', 'version' => process.version, 'command' => nil, @@ -1217,6 +1225,7 @@ 'environment_json' => nil, 'memory' => 1024, 'instances' => 1, + 'log_rate_limit' => 1_048_576, 'disk_quota' => 1024, 'state' => 'STOPPED', 'version' => process.version, @@ -1497,6 +1506,7 @@ def make_actual_lrp(instance_guid:, index:, state:, error:, since:) 'memory' => 1024, 'instances' => 1, 'disk_quota' => 1024, + 'log_rate_limit' => 1_048_576, 'state' => 'STARTED', 'version' => process.version, 'command' => nil, @@ -1852,6 +1862,7 @@ def make_actual_lrp(instance_guid:, index:, state:, error:, since:) 'memory' => 1024, 'instances' => 1, 'disk_quota' => 1024, + 'log_rate_limit' => 1_048_576, 'state' => 'STOPPED', 'version' => process.version, 'command' => nil, diff --git a/spec/request/v2/routes_spec.rb b/spec/request/v2/routes_spec.rb index c34d02d72e3..ae786b43285 100644 --- a/spec/request/v2/routes_spec.rb +++ b/spec/request/v2/routes_spec.rb @@ -147,6 +147,7 @@ 'memory' => 1024, 'instances' => 1, 'disk_quota' => 1024, + 'log_rate_limit' => 1048576, 'state' => 'STOPPED', 'version' => process.version, 'command' => nil, @@ -349,6 +350,7 @@ 'memory' => 1024, 'instances' => 1, 'disk_quota' => 1024, + 'log_rate_limit' => 1048576, 'state' => 'STOPPED', 'version' => process.version, 'command' => nil, diff --git a/spec/request/v2/service_bindings_spec.rb b/spec/request/v2/service_bindings_spec.rb index d8705fff879..7485e5d6745 100644 --- a/spec/request/v2/service_bindings_spec.rb +++ b/spec/request/v2/service_bindings_spec.rb @@ -166,6 +166,7 @@ 'memory' => 1024, 'instances' => 1, 'disk_quota' => 1024, + 'log_rate_limit' => 1048576, 'state' => 'STOPPED', 'version' => process1.version, 'command' => nil, diff --git a/spec/request/v2/spaces_spec.rb b/spec/request/v2/spaces_spec.rb index e69b00436ec..713af2d6554 100644 --- a/spec/request/v2/spaces_spec.rb +++ b/spec/request/v2/spaces_spec.rb @@ -293,6 +293,7 @@ 'health_check_timeout' => nil, 'health_check_type' => 'port', 'instances' => 1, + 'log_rate_limit' => 1_048_576, 'memory' => 1024, 'name' => process.name, 'package_state' => 'STAGED', diff --git a/spec/unit/actions/organization_quotas_create_spec.rb b/spec/unit/actions/organization_quotas_create_spec.rb index 04349c77d44..3efe2d75acc 100644 --- a/spec/unit/actions/organization_quotas_create_spec.rb +++ b/spec/unit/actions/organization_quotas_create_spec.rb @@ -17,7 +17,8 @@ module VCAP::CloudController total_memory_in_mb: 1, per_process_memory_in_mb: 2, total_instances: 3, - per_app_tasks: 4 + per_app_tasks: 4, + log_rate_limit_in_bytes_per_second: 2000 }, services: { paid_services_allowed: false, @@ -57,6 +58,7 @@ module VCAP::CloudController expect(organization_quota.instance_memory_limit).to eq(2) expect(organization_quota.app_instance_limit).to eq(3) expect(organization_quota.app_task_limit).to eq(4) + expect(organization_quota.log_rate_limit).to eq(2000) expect(organization_quota.total_services).to eq(5) expect(organization_quota.total_service_keys).to eq(6) @@ -79,6 +81,7 @@ module VCAP::CloudController expect(organization_quota.instance_memory_limit).to eq(-1) expect(organization_quota.app_instance_limit).to eq(-1) expect(organization_quota.app_task_limit).to eq(-1) + expect(organization_quota.log_rate_limit).to eq(-1) expect(organization_quota.total_services).to eq(-1) expect(organization_quota.total_service_keys).to eq(-1) diff --git a/spec/unit/actions/organization_quotas_update_spec.rb b/spec/unit/actions/organization_quotas_update_spec.rb index f4a9bdefc6e..e75f9151bae 100644 --- a/spec/unit/actions/organization_quotas_update_spec.rb +++ b/spec/unit/actions/organization_quotas_update_spec.rb @@ -15,7 +15,8 @@ module VCAP::CloudController total_memory_in_mb: 5120, per_process_memory_in_mb: 1024, total_instances: 8, - per_app_tasks: nil + per_app_tasks: nil, + log_rate_limit_in_bytes_per_second: 2000 }, services: { paid_services_allowed: false, @@ -49,6 +50,7 @@ module VCAP::CloudController expect(updated_organization_quota.instance_memory_limit).to eq(1024) expect(updated_organization_quota.app_instance_limit).to eq(8) expect(updated_organization_quota.app_task_limit).to eq(-1) + expect(updated_organization_quota.log_rate_limit).to eq(2000) expect(updated_organization_quota.total_services).to eq(10) expect(updated_organization_quota.total_service_keys).to eq(20) @@ -64,6 +66,7 @@ module VCAP::CloudController updated_organization_quota = OrganizationQuotasUpdate.update(org_quota, minimum_message) expect(updated_organization_quota.name).to eq('org_quota_name') + expect(updated_organization_quota.log_rate_limit).to eq(-1) end context 'when a model validation fails' do diff --git a/spec/unit/actions/process_scale_spec.rb b/spec/unit/actions/process_scale_spec.rb index 45688d65dd0..9d56a1a6ba5 100644 --- a/spec/unit/actions/process_scale_spec.rb +++ b/spec/unit/actions/process_scale_spec.rb @@ -4,7 +4,14 @@ module VCAP::CloudController RSpec.describe ProcessScale do subject(:process_scale) { ProcessScale.new(user_audit_info, process, message) } - let(:valid_message_params) { { instances: 2, memory_in_mb: 100, disk_in_mb: 200 } } + let(:valid_message_params) do + { + instances: 2, + memory_in_mb: 100, + disk_in_mb: 200, + log_rate_limit_in_bytes_per_second: 409_600 + } + end let(:message) { ProcessScaleMessage.new(valid_message_params) } let(:app) { AppModel.make } let!(:process) { ProcessModelFactory.make(disk_quota: 50, app: app) } @@ -15,12 +22,14 @@ module VCAP::CloudController expect(process.instances).not_to eq(2) expect(process.memory).not_to eq(100) expect(process.disk_quota).not_to eq(200) + expect(process.log_rate_limit).not_to eq(409_600) process_scale.scale expect(process.reload.instances).to eq(2) expect(process.reload.memory).to eq(100) expect(process.reload.disk_quota).to eq(200) + expect(process.log_rate_limit).to eq(409_600) end it 'does not set instances if the user did not request it' do @@ -50,6 +59,15 @@ module VCAP::CloudController expect(process.disk_quota).to eq(original_value) end + it 'does not set log quota if the user did not request it' do + valid_message_params.delete(:log_rate_limit_in_bytes_per_second) + original_value = process.log_rate_limit + + process_scale.scale + + expect(process.log_rate_limit).to eq(original_value) + end + describe 'audit events' do it 'creates a process audit event' do expect(Repositories::ProcessEventRepository).to receive(:record_scale).with( @@ -58,7 +76,8 @@ module VCAP::CloudController { 'instances' => 2, 'memory_in_mb' => 100, - 'disk_in_mb' => 200 + 'disk_in_mb' => 200, + 'log_rate_limit_in_bytes_per_second' => 409_600, }, manifest_triggered: false ) @@ -76,7 +95,8 @@ module VCAP::CloudController { 'instances' => 2, 'memory_in_mb' => 100, - 'disk_in_mb' => 200 + 'disk_in_mb' => 200, + 'log_rate_limit_in_bytes_per_second' => 409_600, }, manifest_triggered: true ) @@ -109,12 +129,14 @@ module VCAP::CloudController expect(process.instances).to eq(1) expect(process.memory).to eq(1024) expect(process.disk_quota).to eq(50) + expect(process.log_rate_limit).to eq(1_048_576) process_scale.scale expect(process.reload.instances).to eq(2) expect(process.reload.memory).to eq(100) expect(process.reload.disk_quota).to eq(200) + expect(process.reload.log_rate_limit).to eq(409_600) end it 'fails if the process is web' do @@ -123,12 +145,14 @@ module VCAP::CloudController expect(process.instances).to eq(1) expect(process.memory).to eq(1024) expect(process.disk_quota).to eq(50) + expect(process.log_rate_limit).to eq(1_048_576) expect { process_scale.scale }.to raise_error(ProcessScale::InvalidProcess, 'Cannot scale this process while a deployment is in flight.') expect(process.reload.instances).to eq(1) expect(process.reload.memory).to eq(1024) expect(process.reload.disk_quota).to eq(50) + expect(process.reload.log_rate_limit).to eq(1_048_576) end end end diff --git a/spec/unit/actions/space_diff_manifest_spec.rb b/spec/unit/actions/space_diff_manifest_spec.rb index 341e36004c9..b410175fa01 100644 --- a/spec/unit/actions/space_diff_manifest_spec.rb +++ b/spec/unit/actions/space_diff_manifest_spec.rb @@ -21,6 +21,7 @@ module VCAP::CloudController 'instances' => process1.instances, 'memory' => '1024M', 'disk_quota' => '1024M', + 'log_rate_limit_per_second' => '1M', 'health-check-type' => process1.health_check_type }, { @@ -28,6 +29,7 @@ module VCAP::CloudController 'instances' => process2.instances, 'memory' => '1024M', 'disk_quota' => '1024M', + 'log_rate_limit_per_second' => '1M', 'health-check-type' => process2.health_check_type } ] @@ -264,6 +266,7 @@ module VCAP::CloudController before do default_manifest['applications'][0]['processes'][0]['memory'] = '1G' default_manifest['applications'][0]['processes'][0]['disk_quota'] = '1G' + default_manifest['applications'][0]['processes'][0]['log_rate_limit_per_second'] = '1024K' end it 'returns an empty diff' do expect(subject).to eq([]) @@ -274,11 +277,13 @@ module VCAP::CloudController before do default_manifest['applications'][0]['processes'][0]['memory'] = '2G' default_manifest['applications'][0]['processes'][0]['disk_quota'] = '4G' + default_manifest['applications'][0]['processes'][0]['log_rate_limit_per_second'] = '2G' end - it 'returns the diff formatted as megabytes' do + it 'returns the diff formatted' do expect(subject).to eq([ { 'op' => 'replace', 'path' => '/applications/0/processes/0/memory', 'value' => '2048M', 'was' => '1024M' }, { 'op' => 'replace', 'path' => '/applications/0/processes/0/disk_quota', 'value' => '4096M', 'was' => '1024M' }, + { 'op' => 'replace', 'path' => '/applications/0/processes/0/log_rate_limit_per_second', 'value' => '2G', 'was' => '1M' }, ]) end end @@ -320,6 +325,7 @@ module VCAP::CloudController before do default_manifest['applications'][0]['memory'] = '1G' default_manifest['applications'][0]['disk_quota'] = '1G' + default_manifest['applications'][0]['log_rate_limit_per_second'] = '1024K' end it 'returns an empty diff if the field is equivalent' do diff --git a/spec/unit/actions/space_quota_update_spec.rb b/spec/unit/actions/space_quota_update_spec.rb index 86b7feae606..e98a366a899 100644 --- a/spec/unit/actions/space_quota_update_spec.rb +++ b/spec/unit/actions/space_quota_update_spec.rb @@ -23,7 +23,8 @@ module VCAP::CloudController total_memory_in_mb: 5120, per_process_memory_in_mb: 1024, total_instances: 8, - per_app_tasks: nil + per_app_tasks: nil, + log_rate_limit_in_bytes_per_second: 2000, }, services: { paid_services_allowed: false, @@ -50,6 +51,7 @@ module VCAP::CloudController expect(updated_space_quota.instance_memory_limit).to eq(1024) expect(updated_space_quota.app_instance_limit).to eq(8) expect(updated_space_quota.app_task_limit).to eq(-1) + expect(updated_space_quota.log_rate_limit).to eq(2000) expect(updated_space_quota.total_services).to eq(10) expect(updated_space_quota.total_service_keys).to eq(20) @@ -63,6 +65,7 @@ module VCAP::CloudController updated_space_quota = SpaceQuotaUpdate.update(space_quota, minimum_message) expect(updated_space_quota.name).to eq('space_quota_name') + expect(updated_space_quota.log_rate_limit).to eq(-1) end context 'when a model validation fails' do diff --git a/spec/unit/actions/space_quotas_create_spec.rb b/spec/unit/actions/space_quotas_create_spec.rb index aac7774bd0a..a40d9a45032 100644 --- a/spec/unit/actions/space_quotas_create_spec.rb +++ b/spec/unit/actions/space_quotas_create_spec.rb @@ -30,6 +30,7 @@ module VCAP::CloudController per_process_memory_in_mb: 6, total_instances: 7, per_app_tasks: 8, + log_rate_limit_in_bytes_per_second: 2000, }, services: { paid_services_allowed: false, @@ -68,6 +69,7 @@ module VCAP::CloudController expect(space_quota.instance_memory_limit).to eq(-1) expect(space_quota.app_instance_limit).to eq(-1) expect(space_quota.app_task_limit).to eq(-1) + expect(space_quota.log_rate_limit).to eq(-1) expect(space_quota.total_services).to eq(-1) expect(space_quota.total_service_keys).to eq(-1) @@ -90,6 +92,7 @@ module VCAP::CloudController expect(space_quota.instance_memory_limit).to eq(6) expect(space_quota.app_instance_limit).to eq(7) expect(space_quota.app_task_limit).to eq(8) + expect(space_quota.log_rate_limit).to eq(2000) expect(space_quota.total_services).to eq(9) expect(space_quota.total_service_keys).to eq(10) diff --git a/spec/unit/actions/task_create_spec.rb b/spec/unit/actions/task_create_spec.rb index 09119ba6245..d029f136acd 100644 --- a/spec/unit/actions/task_create_spec.rb +++ b/spec/unit/actions/task_create_spec.rb @@ -8,7 +8,8 @@ module VCAP::CloudController Config.new({ maximum_app_disk_in_mb: 4096, default_app_memory: 1024, - default_app_disk_in_mb: 1024 + default_app_disk_in_mb: 1024, + default_app_log_rate_limit_in_bytes_per_second: 999 }) end @@ -193,6 +194,28 @@ module VCAP::CloudController end end + describe 'log_rate_limit' do + context 'when log_rate_limit is specified' do + let(:message) { TaskCreateMessage.new name: name, command: command, log_rate_limit_in_bytes_per_second: 100 } + + it 'returns what is in the message' do + task = task_create_action.create(app, message, user_audit_info) + + expect(task.log_rate_limit).to eq(100) + end + end + + context 'when log_rate_limit is not specified' do + let(:message) { TaskCreateMessage.new name: name, command: command } + + it 'returns the default' do + task = task_create_action.create(app, message, user_audit_info) + + expect(task.log_rate_limit).to eq(999) + end + end + end + describe 'disk_in_mb' do before { config.set(:default_app_disk_in_mb, 200) } @@ -327,6 +350,32 @@ module VCAP::CloudController end end + describe 'log_rate_limit' do + before do + config.set(:default_app_log_rate_limit_in_bytes_per_second, 999) + end + + context 'when there is a template and the message does not specify log_rate_limit_in_bytes_per_second' do + let(:process) { VCAP::CloudController::ProcessModel.make(app: app, type: 'web', log_rate_limit: 23) } + let(:message) { TaskCreateMessage.new(name: name, command: 'ok', disk_in_mb: 2048, template: { process: { guid: process.guid } }) } + + it 'uses the memory from the template process' do + task = task_create_action.create(app, message, user_audit_info) + expect(task.log_rate_limit).to eq(23) + end + end + + context 'when there is a template and the message specifies log_rate_limit_in_bytes_per_second' do + let(:process) { VCAP::CloudController::ProcessModel.make(app: app, type: 'web', log_rate_limit: 23) } + let(:message) { TaskCreateMessage.new(name: name, command: 'ok', log_rate_limit_in_bytes_per_second: 2048, template: { process: { guid: process.guid } }) } + + it 'uses the memory from the message' do + task = task_create_action.create(app, message, user_audit_info) + expect(task.log_rate_limit).to eq(2048) + end + end + end + describe 'disk_in_mb' do before do config.set(:default_app_disk_in_mb, 4096) diff --git a/spec/unit/controllers/runtime/apps_controller_spec.rb b/spec/unit/controllers/runtime/apps_controller_spec.rb index 30ef40adf9f..7e86b70c17d 100644 --- a/spec/unit/controllers/runtime/apps_controller_spec.rb +++ b/spec/unit/controllers/runtime/apps_controller_spec.rb @@ -61,6 +61,7 @@ module VCAP::CloudController console: { type: 'bool', default: false }, debug: { type: 'string' }, disk_quota: { type: 'integer' }, + log_rate_limit: { type: 'integer' }, environment_json: { type: 'hash', default: {} }, health_check_http_endpoint: { type: 'string' }, health_check_timeout: { type: 'integer' }, @@ -88,6 +89,7 @@ module VCAP::CloudController console: { type: 'bool' }, debug: { type: 'string' }, disk_quota: { type: 'integer' }, + log_rate_limit: { type: 'integer' }, environment_json: { type: 'hash' }, health_check_http_endpoint: { type: 'string' }, health_check_timeout: { type: 'integer' }, diff --git a/spec/unit/lib/cloud_controller/app_manifest/byte_converter_spec.rb b/spec/unit/lib/cloud_controller/app_manifest/byte_converter_spec.rb index 8c9d8274f1a..bbc0fff0073 100644 --- a/spec/unit/lib/cloud_controller/app_manifest/byte_converter_spec.rb +++ b/spec/unit/lib/cloud_controller/app_manifest/byte_converter_spec.rb @@ -126,5 +126,77 @@ module VCAP::CloudController end end end + + describe '#convert_to_b' do + context 'when given 1M' do + let(:byte_value) { '1M' } + + it 'returns the value in bytes' do + expect(subject.convert_to_b(byte_value)).to eq(1_048_576) + end + end + end + + describe '#human_readable_byte_value' do + context 'when given nil' do + let(:byte_value) { nil } + + it 'returns nil' do + expect(subject.human_readable_byte_value(byte_value)).to be_nil + end + end + + context 'when given 1M in bytes' do + let(:byte_value) { 1_048_576 } + + it 'returns the human readable value' do + expect(subject.human_readable_byte_value(byte_value)).to eq('1M') + end + end + + context 'when given 1G in bytes' do + let(:byte_value) { 1_073_741_824 } + + it 'returns the human readable value' do + expect(subject.human_readable_byte_value(byte_value)).to eq('1G') + end + end + + context 'when given 1.1M in bytes' do + let(:byte_value) { 1_153_434 } + + it 'returns the human readable value in bytes to avoid losing precision' do + expect(subject.human_readable_byte_value(byte_value)).to eq('1153434B') + end + end + + context 'when given 1M + 1K' do + let(:byte_value) { 1049600 } + + it 'returns the human readable value in kilobytes to avoid losing precision' do + expect(subject.human_readable_byte_value(byte_value)).to eq('1025K') + end + end + + context 'when given a string' do + let(:byte_value) { 'not-a-number' } + + it 'raises an error' do + expect { + subject.human_readable_byte_value(byte_value) + }.to raise_error(ByteConverter::InvalidBytesError) + end + end + + context 'when given a float' do + let(:byte_value) { 1.1 } + + it 'raises an error' do + expect { + subject.human_readable_byte_value(byte_value) + }.to raise_error(ByteConverter::InvalidBytesError) + end + end + end end end diff --git a/spec/unit/lib/cloud_controller/app_services/process_log_rate_limit_calculator_spec.rb b/spec/unit/lib/cloud_controller/app_services/process_log_rate_limit_calculator_spec.rb new file mode 100644 index 00000000000..bfbd21af3f4 --- /dev/null +++ b/spec/unit/lib/cloud_controller/app_services/process_log_rate_limit_calculator_spec.rb @@ -0,0 +1,132 @@ +require 'spec_helper' + +module VCAP::CloudController + RSpec.describe ProcessLogRateLimitCalculator do + subject { ProcessLogRateLimitCalculator.new(process) } + let(:process_guid) { 'i-do-not-match-the-app-guid' } + let(:app_model) { AppModel.make } + let(:process) { ProcessModel.make(guid: process_guid, app: app_model) } + let(:stopped_state) { 'STOPPED' } + let(:started_state) { 'STARTED' } + + describe '#additional_log_rate_limit_requested' do + context 'when the app state is STOPPED' do + before do + process.state = stopped_state + process.save(validate: false) + end + + it 'returns 0' do + expect(subject.additional_log_rate_limit_requested).to eq(0) + end + end + + context 'when the app state is STARTED' do + let(:process) { ProcessModel.make(state: started_state, guid: process_guid, app: app_model) } + + context 'and the app is already in the db' do + it 'raises ApplicationMissing if the app no longer exists in the db' do + process.delete + expect { subject.additional_log_rate_limit_requested }.to raise_error(CloudController::Errors::ApplicationMissing) + end + + context 'and it is changing from STOPPED' do + before do + db_app = ProcessModel.find(guid: process.guid) + db_app.state = stopped_state + db_app.save(validate: false) + end + + it 'returns the total requested log_rate_limit' do + process.state = started_state + expect(subject.additional_log_rate_limit_requested).to eq(subject.total_requested_log_rate_limit) + end + end + + context 'and the app is already STARTED' do + before do + db_app = ProcessModel.find(guid: process.guid) + db_app.state = started_state + db_app.save(validate: false) + end + + it 'returns only newly requested log_rate_limit' do + expected = process.log_rate_limit + process.instances += 1 + + expect(subject.additional_log_rate_limit_requested).to eq(expected) + end + end + end + + context 'and the app is new' do + let(:process) { ProcessModel.new } + before do + process.instances = 1 + process.log_rate_limit = 100 + end + + it 'returns the total requested log_rate_limit' do + process.state = started_state + expect(subject.additional_log_rate_limit_requested).to eq(subject.total_requested_log_rate_limit) + end + end + end + + context 'and the app is requesting unlimited log quota' do + let(:process) { ProcessModel.new } + before do + process.instances = 1 + process.log_rate_limit = -1 + end + + it 'returns the fact that it is requesting unlimited quota' do + process.state = started_state + expect(subject.additional_log_rate_limit_requested).to eq(-1) + end + end + end + + describe '#total_requested_log_rate_limit' do + it 'returns requested log_rate_limit * requested instances' do + expected = process.log_rate_limit * process.instances + expect(subject.total_requested_log_rate_limit).to eq(expected) + end + end + + describe '#currently_used_log_rate_limit' do + context 'when the app is new' do + let(:process) { ProcessModel.new } + it 'returns 0' do + expect(subject.currently_used_log_rate_limit).to eq(0) + end + end + + it 'raises ApplicationMissing if the app no longer exists in the db' do + process.delete + expect { subject.currently_used_log_rate_limit }.to raise_error(CloudController::Errors::ApplicationMissing) + end + + context 'when the app in the db is STOPPED' do + it 'returns 0' do + expect(subject.currently_used_log_rate_limit).to eq(0) + end + end + + context 'when the app in the db is STARTED' do + before do + process.state = started_state + process.save(validate: false) + end + + it 'returns the log_rate_limit * instances of the db row' do + expected = process.instances * process.log_rate_limit + process.instances += 5 + process.log_rate_limit += 100 + + expect(subject.currently_used_log_rate_limit).to eq(expected) + end + end + end + end +end diff --git a/spec/unit/messages/app_manifest_message_spec.rb b/spec/unit/messages/app_manifest_message_spec.rb index caf0995f231..887f7cea778 100644 --- a/spec/unit/messages/app_manifest_message_spec.rb +++ b/spec/unit/messages/app_manifest_message_spec.rb @@ -104,6 +104,67 @@ module VCAP::CloudController end end + describe 'log_rate_limit_per_second' do + context 'when log_rate_limit_per_second unit is not part of expected set of values' do + let(:params_from_yaml) { { name: 'eugene', log_rate_limit_per_second: '200INVALID' } } + + it 'is not valid' do + message = AppManifestMessage.create_from_yml(params_from_yaml) + + expect(message).not_to be_valid + expect(message.errors).to have(1).items + expect(message.errors.full_messages).to include( + 'Process "web": Log rate limit per second must use a supported unit: B, K, KB, M, MB, G, GB, T, or TB' + ) + end + end + + context 'when log_rate_limit_per_second is less than -1 bytes' do + let(:params_from_yaml) { { name: 'eugene', log_rate_limit_per_second: '-1M' } } + + it 'is not valid' do + message = AppManifestMessage.create_from_yml(params_from_yaml) + + expect(message).not_to be_valid + expect(message.errors).to have(1).items + expect(message.errors.full_messages).to include('Process "web": Log rate limit must be an integer greater than or equal to -1B') + end + end + + context 'when log_rate_limit_per_second is not numeric' do + let(:params_from_yaml) { { name: 'eugene', log_rate_limit_per_second: 'gerg herscheisers' } } + + it 'is not valid' do + message = AppManifestMessage.create_from_yml(params_from_yaml) + + expect(message).not_to be_valid + expect(message.errors).to have(1).items + expect(message.errors.full_messages).to include( + 'Process "web": Log rate limit per second is not a number' + ) + end + end + + context 'when log_rate_limit_per_second is unlimited amount' do + let(:params_from_yaml) { { name: 'eugene', log_rate_limit_per_second: '-1B' } } + + it 'is valid' do + message = AppManifestMessage.create_from_yml(params_from_yaml) + expect(message).to be_valid + end + end + + context 'when log_rate_limit_per_second is in megabytes' do + let(:params_from_yaml) { { name: 'eugene', log_rate_limit_per_second: '1MB' } } + + it 'is valid' do + message = AppManifestMessage.create_from_yml(params_from_yaml) + expect(message).to be_valid + end + end + + end + describe 'buildpack' do context 'when providing a valid buildpack name' do let(:buildpack) { Buildpack.make } @@ -572,6 +633,7 @@ module VCAP::CloudController 'instances' => -30, 'memory' => 'potato', 'disk_quota' => '100', + 'log_rate_limit_per_second' => 'kumara', 'health_check_type' => 'sweet_potato', 'health_check_http_endpoint' => '/healthcheck_potato', 'health_check_invocation_timeout' => 'yucca', @@ -585,6 +647,7 @@ module VCAP::CloudController 'instances' => 'cassava', 'memory' => 'potato', 'disk_quota' => '100', + 'log_rate_limit_per_second' => '100', 'health_check_type' => 'sweet_potato', 'health_check_http_endpoint' => '/healthcheck_potato', 'health_check_invocation_timeout' => 'yucca', @@ -602,10 +665,11 @@ module VCAP::CloudController it 'includes the type of the process in the error message' do message = AppManifestMessage.create_from_yml(params_from_yaml) expect(message).to_not be_valid - expect(message.errors).to have(16).items + expect(message.errors).to have(18).items expect(message.errors.full_messages).to match_array([ 'Process "type1": Command must be between 1 and 4096 characters', 'Process "type1": Disk quota must use a supported unit: B, K, KB, M, MB, G, GB, T, or TB', + 'Process "type1": Log rate limit per second is not a number', 'Process "type1": Instances must be greater than or equal to 0', 'Process "type1": Memory is not a number', 'Process "type1": Timeout is not a number', @@ -614,6 +678,7 @@ module VCAP::CloudController 'Process "type1": Health check invocation timeout is not a number', 'Process "type2": Command must be between 1 and 4096 characters', 'Process "type2": Disk quota must use a supported unit: B, K, KB, M, MB, G, GB, T, or TB', + 'Process "type2": Log rate limit per second must use a supported unit: B, K, KB, M, MB, G, GB, T, or TB', 'Process "type2": Instances is not a number', 'Process "type2": Memory is not a number', 'Process "type2": Timeout is not a number', diff --git a/spec/unit/messages/manifest_process_scale_message_spec.rb b/spec/unit/messages/manifest_process_scale_message_spec.rb index 059924ba94c..5f31604935a 100644 --- a/spec/unit/messages/manifest_process_scale_message_spec.rb +++ b/spec/unit/messages/manifest_process_scale_message_spec.rb @@ -141,6 +141,44 @@ module VCAP::CloudController end end + describe '#log_rate_limit' do + context 'when log_rate_limit is not an number' do + let(:params) { { log_rate_limit: 'silly string thing' } } + + it 'is not valid' do + message = ManifestProcessScaleMessage.new(params) + + expect(message).not_to be_valid + expect(message.errors.count).to eq(1) + expect(message.errors.full_messages).to include('Log rate limit must be an integer greater than or equal to -1B') + end + end + + context 'when log_rate_limit is < -1' do + let(:params) { { log_rate_limit: -2 } } + + it 'is not valid' do + message = ManifestProcessScaleMessage.new(params) + + expect(message).not_to be_valid + expect(message.errors.count).to eq(1) + expect(message.errors.full_messages).to include('Log rate limit must be an integer greater than or equal to -1B') + end + end + + context 'when log_rate_limit is not an integer' do + let(:params) { { log_rate_limit: 3.5 } } + + it 'is not valid' do + message = ManifestProcessScaleMessage.new(params) + + expect(message).not_to be_valid + expect(message.errors.count).to eq(1) + expect(message.errors.full_messages).to include('Log rate limit must be an integer greater than or equal to -1B') + end + end + end + context 'when we have more than one error' do let(:params) { { disk_quota: 3.5, memory: 'smiling greg' } } @@ -161,7 +199,7 @@ module VCAP::CloudController let(:manifest_message) { ManifestProcessScaleMessage.new(params) } context 'when all params are given' do - let(:params) { { instances: 3, memory: 1024, disk_quota: 2048 } } + let(:params) { { instances: 3, memory: 1024, disk_quota: 2048, log_rate_limit: 1024 } } it 'returns a process_scale_message with the appropriate values' do scale_message = manifest_message.to_process_scale_message @@ -169,6 +207,7 @@ module VCAP::CloudController expect(scale_message.instances).to eq(3) expect(scale_message.memory_in_mb).to eq(1024) expect(scale_message.disk_in_mb).to eq(2048) + expect(scale_message.log_rate_limit_in_bytes_per_second).to eq(1024) end end @@ -184,6 +223,16 @@ module VCAP::CloudController end end + context 'when no log_rate_limit is given' do + let(:params) { { instances: 3, memory: 1024 } } + + it 'does not set anything for log_rate_limit_in_bytes_per_second' do + scale_message = manifest_message.to_process_scale_message + + expect(scale_message.log_rate_limit_in_bytes_per_second).to be_falsey + end + end + context 'when no instances is given' do let(:params) { { memory: 1024, disk_quota: 2048 } } diff --git a/spec/unit/messages/organization_quotas_create_message_spec.rb b/spec/unit/messages/organization_quotas_create_message_spec.rb index 38122e4c0af..5d183fe1c8a 100644 --- a/spec/unit/messages/organization_quotas_create_message_spec.rb +++ b/spec/unit/messages/organization_quotas_create_message_spec.rb @@ -240,6 +240,74 @@ module VCAP::CloudController end end + describe 'log_rate_limit_in_bytes_per_second' do + context 'when the type is a string' do + let(:params) { + { + name: 'my-name', + apps: { log_rate_limit_in_bytes_per_second: 'bob' }, + relationships: relationships, + } + } + + it 'is not valid' do + expect(subject).to be_invalid + expect(subject.errors[:apps]).to contain_exactly('Log rate limit in bytes per second is not a number') + end + end + context 'when the type is decimal' do + let(:params) { + { + name: 'my-name', + apps: { log_rate_limit_in_bytes_per_second: 1.1 }, + relationships: relationships, + } + } + + it 'is not valid' do + expect(subject).to be_invalid + expect(subject.errors[:apps]).to contain_exactly('Log rate limit in bytes per second must be an integer') + end + end + context 'when the type is a negative integer' do + let(:params) { + { + name: 'my-name', + apps: { log_rate_limit_in_bytes_per_second: -1 }, + relationships: relationships, + } + } + + it 'is not valid because "unlimited" is set with null, not -1, in V3' do + expect(subject).to be_invalid + expect(subject.errors[:apps]).to contain_exactly('Log rate limit in bytes per second must be greater than or equal to 0') + end + end + + context 'when the type is zero' do + let(:params) { + { + name: 'my-name', + apps: { log_rate_limit_in_bytes_per_second: 0 }, + relationships: relationships, + } + } + + it { is_expected.to be_valid } + end + context 'when the type is nil (unlimited)' do + let(:params) { + { + name: 'my-name', + apps: { log_rate_limit_in_bytes_per_second: nil }, + relationships: relationships, + } + } + + it { is_expected.to be_valid } + end + end + describe 'total_instances' do context 'when the type is a string' do let(:params) { diff --git a/spec/unit/messages/process_scale_message_spec.rb b/spec/unit/messages/process_scale_message_spec.rb index 8de05bbf7d4..0a45eeda210 100644 --- a/spec/unit/messages/process_scale_message_spec.rb +++ b/spec/unit/messages/process_scale_message_spec.rb @@ -158,5 +158,53 @@ module VCAP::CloudController expect(message.errors[:disk_in_mb]).to include('must be an integer') end end + + context 'when log_rate_limit_in_bytes_per_second is not an number' do + let(:params) { { log_rate_limit_in_bytes_per_second: 'silly string thing' } } + + it 'is not valid' do + message = ProcessScaleMessage.new(params) + + expect(message).not_to be_valid + expect(message.errors.count).to eq(1) + expect(message.errors[:log_rate_limit_in_bytes_per_second]).to include('is not a number') + end + end + + context 'when log_rate_limit_in_bytes_per_second is < -1' do + let(:params) { { log_rate_limit_in_bytes_per_second: -2 } } + + it 'is not valid' do + message = ProcessScaleMessage.new(params) + + expect(message).not_to be_valid + expect(message.errors.count).to eq(1) + expect(message.errors[:log_rate_limit_in_bytes_per_second]).to include('must be greater than or equal to -1') + end + end + + context 'when log_rate_limit_in_bytes_per_second is > the max value allowed in the database' do + let(:params) { { log_rate_limit_in_bytes_per_second: BaseMessage::MAX_DB_BIGINT + 1 } } + + it 'is not valid' do + message = ProcessScaleMessage.new(params) + + expect(message).not_to be_valid + expect(message.errors.count).to eq(1) + expect(message.errors[:log_rate_limit_in_bytes_per_second]).to include('must be less than or equal to 9223372036854775807') + end + end + + context 'when log_rate_limit_in_bytes_per_second is not an integer' do + let(:params) { { log_rate_limit_in_bytes_per_second: 3.5 } } + + it 'is not valid' do + message = ProcessScaleMessage.new(params) + + expect(message).not_to be_valid + expect(message.errors.count).to eq(1) + expect(message.errors[:log_rate_limit_in_bytes_per_second]).to include('must be an integer') + end + end end end diff --git a/spec/unit/messages/quotas_apps_message_spec.rb b/spec/unit/messages/quotas_apps_message_spec.rb index 77e215314c5..6b3caff77a3 100644 --- a/spec/unit/messages/quotas_apps_message_spec.rb +++ b/spec/unit/messages/quotas_apps_message_spec.rb @@ -256,6 +256,68 @@ module VCAP::CloudController end end end + + describe 'log_rate_limit_in_bytes_per_second' do + context 'when the type is a string' do + let(:params) do + { log_rate_limit_in_bytes_per_second: 'bob' } + end + + it 'is not valid' do + expect(subject).to be_invalid + expect(subject.errors).to contain_exactly('Log rate limit in bytes per second is not a number') + end + end + + context 'when the type is decimal' do + let(:params) do + { log_rate_limit_in_bytes_per_second: 1.1 } + end + + it 'is not valid' do + expect(subject).to be_invalid + expect(subject.errors).to contain_exactly('Log rate limit in bytes per second must be an integer') + end + end + + context 'when the type is a negative integer' do + let(:params) do + { log_rate_limit_in_bytes_per_second: -1 } + end + + it 'is not valid because "unlimited" is set with null, not -1, in V3' do + expect(subject).to be_invalid + expect(subject.errors).to contain_exactly('Log rate limit in bytes per second must be greater than or equal to 0') + end + end + + context 'when the type is zero' do + let(:params) do + { log_rate_limit_in_bytes_per_second: 0 } + end + + it { is_expected.to be_valid } + end + + context 'when the type is nil (unlimited)' do + let(:params) do + { log_rate_limit_in_bytes_per_second: nil } + end + + it { is_expected.to be_valid } + end + + context 'when the value is greater than the maximum allowed value in the DB' do + let(:params) do + { log_rate_limit_in_bytes_per_second: 2**64 } + end + + it 'is not valid' do + expect(subject).to be_invalid + expect(subject.errors).to contain_exactly('Log rate limit in bytes per second must be less than or equal to 9223372036854775807') + end + end + end end end end diff --git a/spec/unit/messages/space_quota_update_message_spec.rb b/spec/unit/messages/space_quota_update_message_spec.rb index bd0cba290cf..d8b0adaccd6 100644 --- a/spec/unit/messages/space_quota_update_message_spec.rb +++ b/spec/unit/messages/space_quota_update_message_spec.rb @@ -20,6 +20,7 @@ module VCAP::CloudController per_process_memory_in_mb: 1024, total_instances: 2, per_app_tasks: 4, + log_rate_limit_in_bytes_per_second: 2000, } end @@ -49,6 +50,7 @@ module VCAP::CloudController expect(subject.per_process_memory_in_mb).to eq(1024) expect(subject.total_instances).to eq(2) expect(subject.per_app_tasks).to eq(4) + expect(subject.log_rate_limit_in_bytes_per_second).to eq(2000) expect(subject.paid_services_allowed).to be_truthy expect(subject.total_service_instances).to eq(17) expect(subject.total_service_keys).to eq(19) diff --git a/spec/unit/messages/task_create_message_spec.rb b/spec/unit/messages/task_create_message_spec.rb index 45635e04e9b..fac1a3d449e 100644 --- a/spec/unit/messages/task_create_message_spec.rb +++ b/spec/unit/messages/task_create_message_spec.rb @@ -141,6 +141,59 @@ module VCAP::CloudController end end + describe 'log_rate_limit_in_bytes_per_second' do + it 'can be nil' do + body.delete 'log_rate_limit_in_bytes_per_second' + + message = TaskCreateMessage.new(body) + + expect(message).to be_valid + end + + it 'must be numerical' do + body['log_rate_limit_in_bytes_per_second'] = 'trout' + + message = TaskCreateMessage.new(body) + + expect(message).to_not be_valid + expect(message.errors.full_messages).to include('Log rate limit in bytes per second is not a number') + end + + it 'may not have a floating point' do + body['log_rate_limit_in_bytes_per_second'] = 4.5 + + message = TaskCreateMessage.new(body) + + expect(message).to_not be_valid + expect(message.errors.full_messages).to include('Log rate limit in bytes per second must be an integer') + end + + it 'may be -1' do + body['log_rate_limit_in_bytes_per_second'] = -1 + + message = TaskCreateMessage.new(body) + + expect(message).to be_valid + end + + it 'may be zero' do + body['log_rate_limit_in_bytes_per_second'] = 0 + + message = TaskCreateMessage.new(body) + + expect(message).to be_valid + end + + it 'may not be smaller than -1' do + body['log_rate_limit_in_bytes_per_second'] = -2 + + message = TaskCreateMessage.new(body) + + expect(message).to_not be_valid + expect(message.errors.full_messages).to include('Log rate limit in bytes per second must be greater than -2') + end + end + describe 'template' do it 'can be nil' do body.delete 'template' diff --git a/spec/unit/models/runtime/constraints/max_log_rate_limit_policy_spec.rb b/spec/unit/models/runtime/constraints/max_log_rate_limit_policy_spec.rb new file mode 100644 index 00000000000..a9c3c075e9e --- /dev/null +++ b/spec/unit/models/runtime/constraints/max_log_rate_limit_policy_spec.rb @@ -0,0 +1,162 @@ +require 'spec_helper' + +RSpec.describe 'max log_rate_limit policies' do + let(:org_or_space) { double(:org_or_space, has_remaining_log_rate_limit: false) } + let(:error_name) { :random_log_rate_limit_error } + + describe AppMaxLogRateLimitPolicy do + subject(:validator) { AppMaxLogRateLimitPolicy.new(process, org_or_space, error_name) } + + context 'when the app specifies a log quota' do + let(:process) { VCAP::CloudController::ProcessModelFactory.make(log_rate_limit: 100, state: 'STOPPED') } + + context 'when the app is being started' do + before do + process.state = 'STARTED' + process.log_rate_limit = 150 + end + + it 'registers an error when quota is exceeded' do + expect(org_or_space).to receive(:has_remaining_log_rate_limit).with(150).and_return(false) + expect(validator).to validate_with_error(process, :log_rate_limit, error_name) + end + + it 'does not register an error when quota is not exceeded' do + expect(org_or_space).to receive(:has_remaining_log_rate_limit).with(150).and_return(true) + expect(validator).to validate_without_error(process) + end + + it 'adds the given error to the model' do + expect(org_or_space).to receive(:has_remaining_log_rate_limit).with(150).and_return(false) + validator.validate + expect(process.errors.on(:log_rate_limit)).to include(error_name) + end + end + + context 'when the app is already started' do + let(:process) { VCAP::CloudController::ProcessModelFactory.make(log_rate_limit: 100, state: 'STARTED') } + + it 'does not register an error when quota is exceeded' do + expect(validator).to validate_without_error(process) + end + + context 'when the instance count has changed and would exceed the quota' do + before do + process.instances = 5 + end + it 'registers an error' do + expect(org_or_space).to receive(:has_remaining_log_rate_limit).with(400).and_return(false) + expect(validator).to validate_with_error(process, :log_rate_limit, error_name) + end + end + end + + context 'when the app is being stopped' do + before do + process.state = 'STOPPED' + end + + it 'does not register an error' do + expect(validator).to validate_without_error(process) + end + + context 'when the instance count has changed and would exceed the quota' do + before do + process.instances = 5 + end + it 'does not register an error' do + expect(validator).to validate_without_error(process) + end + end + end + end + + context 'when the app does not specify a log quota' do + let(:process) { VCAP::CloudController::ProcessModelFactory.make(log_rate_limit: -1, state: 'STOPPED') } + before do + process.state = 'STARTED' + end + + context 'when the org specifies a log quota' do + before do + allow(org_or_space).to receive(:name).and_return('some-org') + allow(org_or_space).to receive(:log_rate_limit).and_return(5000) + end + + it 'is unhappy and adds an error' do + validator.validate + expect(process.errors.on(:log_rate_limit)).to include("cannot be unlimited in organization 'some-org'.") + end + end + + context 'when the space specifies a log quota' do + before do + allow(org_or_space).to receive(:name).and_return('some-space') + allow(org_or_space).to receive(:log_rate_limit).and_return(5000) + allow(org_or_space).to receive(:organization_guid).and_return('some-org-guid') + end + + it 'is unhappy and adds an error' do + validator.validate + expect(process.errors.on(:log_rate_limit)).to include("cannot be unlimited in space 'some-space'.") + end + end + + context 'when both the space and the org specify an infinite log quota' do + before do + expect(org_or_space).to receive(:log_rate_limit).and_return(VCAP::CloudController::QuotaDefinition::UNLIMITED) + expect(org_or_space).to receive(:has_remaining_log_rate_limit).and_return(true) + end + + it 'is happy' do + expect(validator).to validate_without_error(process) + end + end + end + end + + describe TaskMaxLogRateLimitPolicy do + subject(:validator) { TaskMaxLogRateLimitPolicy.new(task, org_or_space, error_name) } + + let(:task) { VCAP::CloudController::TaskModel.make log_rate_limit: 150, state: VCAP::CloudController::TaskModel::RUNNING_STATE } + + context 'when not cancelling a task' do + it 'registers error when quota is exceeded' do + allow(org_or_space).to receive(:has_remaining_log_rate_limit).with(150).and_return(false) + expect(validator).to validate_with_error(task, :log_rate_limit, error_name) + end + + it 'does not register error when quota is not exceeded' do + allow(org_or_space).to receive(:has_remaining_log_rate_limit).with(150).and_return(true) + expect(validator).to validate_without_error(task) + end + + it 'adds the given error to the model' do + allow(org_or_space).to receive(:has_remaining_log_rate_limit).with(150).and_return(false) + validator.validate + expect(task.errors.on(:log_rate_limit)).to include(error_name) + end + end + + context 'when canceling a task' do + it 'does not register an error' do + task.state = VCAP::CloudController::TaskModel::CANCELING_STATE + expect(validator).to validate_without_error(task) + end + end + + context 'when the task is SUCCEEDED' do + it 'does not register error' do + task.state = VCAP::CloudController::TaskModel::SUCCEEDED_STATE + expect(validator).to validate_without_error(task) + end + end + + context 'when the task is FAILED' do + it 'does not register error' do + task.state = VCAP::CloudController::TaskModel::FAILED_STATE + expect(validator).to validate_without_error(task) + end + end + end +end diff --git a/spec/unit/models/runtime/constraints/min_log_rate_limit_policy_spec.rb b/spec/unit/models/runtime/constraints/min_log_rate_limit_policy_spec.rb new file mode 100644 index 00000000000..c106da7283d --- /dev/null +++ b/spec/unit/models/runtime/constraints/min_log_rate_limit_policy_spec.rb @@ -0,0 +1,27 @@ +require 'spec_helper' + +RSpec.describe MinLogRateLimitPolicy do + let(:process) { VCAP::CloudController::ProcessModelFactory.make } + + subject(:validator) { MinLogRateLimitPolicy.new(process) } + + it 'when requested size is negative 2' do + allow(process).to receive(:log_rate_limit).and_return(-2) + expect(validator).to validate_with_error(process, :log_rate_limit, MinLogRateLimitPolicy::ERROR_MSG) + end + + it 'when requested size is negative 1' do + allow(process).to receive(:log_rate_limit).and_return(-1) + expect(validator).to validate_without_error(process) + end + + it 'when requested size is zero' do + allow(process).to receive(:log_rate_limit).and_return(0) + expect(validator).to validate_without_error(process) + end + + it 'when requested size is positive' do + allow(process).to receive(:log_rate_limit).and_return(1) + expect(validator).to validate_without_error(process) + end +end diff --git a/spec/unit/models/runtime/organization_spec.rb b/spec/unit/models/runtime/organization_spec.rb index 8ff2b5b450a..84ef6da4a84 100644 --- a/spec/unit/models/runtime/organization_spec.rb +++ b/spec/unit/models/runtime/organization_spec.rb @@ -508,6 +508,59 @@ module VCAP::CloudController end end + describe '#has_remaining_log_rate_limit' do + let(:log_rate_limit) { 10 } + let(:quota) { QuotaDefinition.make(log_rate_limit: log_rate_limit) } + let(:org) { Organization.make(quota_definition: quota) } + let(:org2) { Organization.make(quota_definition: quota) } + let(:space) { Space.make(organization: org) } + let(:space2) { Space.make(organization: org) } + let(:space_org2) { Space.make(organization: org2) } + let!(:app_model) { AppModel.make(space: space2) } + + context 'when the quota is unlimited' do + let(:log_rate_limit) { QuotaDefinition::UNLIMITED } + + it 'handles large log quotas' do + expect(org.has_remaining_log_rate_limit(10_000_000)).to be_truthy + end + end + + context 'when nothing is running' do + it 'uses the log_rate_limit' do + expect(org.has_remaining_log_rate_limit(10)).to be_truthy + expect(org.has_remaining_log_rate_limit(11)).to be_falsey + end + end + + context 'when something else is running' do + it 'takes all things in the org into account' do + ProcessModelFactory.make(space: space, log_rate_limit: 5, state: 'STARTED') + expect(org.has_remaining_log_rate_limit(5)).to be_truthy + expect(org.has_remaining_log_rate_limit(6)).to be_falsey + + ProcessModelFactory.make(space: space, log_rate_limit: 1, state: 'STARTED') + expect(org.has_remaining_log_rate_limit(4)).to be_truthy + expect(org.has_remaining_log_rate_limit(5)).to be_falsey + + TaskModel.make(app: app_model, log_rate_limit: 1, state: TaskModel::RUNNING_STATE) + expect(org.has_remaining_log_rate_limit(3)).to be_truthy + expect(org.has_remaining_log_rate_limit(4)).to be_falsey + end + + context 'when something else is running in another org' do + it 'only accounts for things running in the owning org' do + ProcessModelFactory.make(space: space_org2, log_rate_limit: 1, instances: 2, state: 'STARTED') + expect(org.has_remaining_log_rate_limit(10)).to be_truthy + expect(org.has_remaining_log_rate_limit(11)).to be_falsey + + expect(org2.has_remaining_log_rate_limit(8)).to be_truthy + expect(org2.has_remaining_log_rate_limit(9)).to be_falsey + end + end + end + end + describe '#instance_memory_limit' do let(:quota) { QuotaDefinition.make(instance_memory_limit: 50) } let(:org) { Organization.make quota_definition: quota } diff --git a/spec/unit/models/runtime/process_model_spec.rb b/spec/unit/models/runtime/process_model_spec.rb index 7049f84707c..e571187cd86 100644 --- a/spec/unit/models/runtime/process_model_spec.rb +++ b/spec/unit/models/runtime/process_model_spec.rb @@ -74,6 +74,11 @@ def expect_no_validator(validator_class) expect(process.ports).to eq([8081, 8082]) end end + + it 'has a default log_rate_limit' do + TestConfig.override(default_app_log_rate_limit_in_bytes_per_second: 873565) + expect(process.log_rate_limit).to eq(873565) + end end describe 'Associations' do @@ -193,6 +198,7 @@ def expect_no_validator(validator_class) expect_validator(InstancesPolicy) expect_validator(MaxDiskQuotaPolicy) expect_validator(MinDiskQuotaPolicy) + expect_validator(MinLogRateLimitPolicy) expect_validator(MinMemoryPolicy) expect_validator(AppMaxInstanceMemoryPolicy) expect_validator(InstancesPolicy) @@ -289,6 +295,15 @@ def expect_no_validator(validator_class) end end + describe 'log_rate_limit' do + subject(:process) { ProcessModelFactory.make } + + it 'does not allow a log_rate_limit below the minimum' do + process.log_rate_limit = -2 + expect(process).to_not be_valid + end + end + describe 'health_check_http_endpoint' do subject(:process) { ProcessModelFactory.make } @@ -402,11 +417,12 @@ def expect_no_validator(validator_class) describe 'quota' do subject(:process) { ProcessModelFactory.make } + let(:log_rate_limit) { 1024 } let(:quota) do - QuotaDefinition.make(memory_limit: 128) + QuotaDefinition.make(memory_limit: 128, log_rate_limit: log_rate_limit) end let(:space_quota) do - SpaceQuotaDefinition.make(memory_limit: 128, organization: org) + SpaceQuotaDefinition.make(memory_limit: 128, organization: org, log_rate_limit: log_rate_limit) end context 'app update' do @@ -420,15 +436,52 @@ def act_as_cf_admin let(:org) { Organization.make(quota_definition: quota) } let(:space) { Space.make(name: 'hi', organization: org, space_quota_definition: space_quota) } let(:parent_app) { AppModel.make(space: space) } - subject!(:process) { ProcessModelFactory.make(app: parent_app, memory: 64, instances: 2, state: 'STARTED') } + subject!(:process) { ProcessModelFactory.make(app: parent_app, memory: 64, log_rate_limit: 512, instances: 2, state: 'STOPPED') } it 'should raise error when quota is exceeded' do process.memory = 65 - expect { process.save }.to raise_error(/quota_exceeded/) + process.state = 'STARTED' + expect { process.save }.to raise_error(/memory quota_exceeded/) + end + + it 'should raise error when log quota is exceeded' do + number = (log_rate_limit / 2) + 1 + process.log_rate_limit = number + process.state = 'STARTED' + expect { process.save }.to raise_error(/exceeds space log rate quota/) + end + + context 'when only exceeding the org quota' do + before do + org.quota_definition = QuotaDefinition.make(log_rate_limit: 5) + org.save + end + + it 'raises an error' do + process.log_rate_limit = 10 + process.state = 'STARTED' + expect { process.save }.to raise_error(/exceeds organization log rate quota/) + end + end + + it 'should not raise error when log quota is not exceeded' do + number = (log_rate_limit / 2) + process.log_rate_limit = number + process.state = 'STARTED' + expect { process.save }.not_to raise_error + end + + it 'should raise an error when starting an app with unlimited log rate and a limited quota' do + process.log_rate_limit = -1 + process.state = 'STARTED' + expect { process.save }.to raise_error(Sequel::ValidationFailed) + expect(process.errors.on(:log_rate_limit)).to include("cannot be unlimited in organization '#{org.name}'.") + expect(process.errors.on(:log_rate_limit)).to include("cannot be unlimited in space '#{space.name}'.") end it 'should not raise error when quota is not exceeded' do process.memory = 63 + process.state = 'STARTED' expect { process.save }.to_not raise_error end @@ -436,12 +489,15 @@ def act_as_cf_admin quota.memory_limit = 32 quota.save process.memory = 100 + process.state = 'STARTED' process.save(validate: false) expect(process.reload).to_not be_valid expect { process.delete }.not_to raise_error end it 'allows scaling down instances of an app from above quota to below quota' do + process.update(state: 'STARTED') + org.quota_definition = QuotaDefinition.make(memory_limit: 72) act_as_cf_admin { org.save } @@ -460,6 +516,7 @@ def act_as_cf_admin quota.save process.instances = 5 + process.state = 'STARTED' expect { process.save }.to raise_error(/instance_limit_exceeded/) end @@ -471,10 +528,13 @@ def act_as_cf_admin quota.save process.instances = 5 + process.state = 'STARTED' expect { process.save }.to raise_error(/instance_limit_exceeded/) end it 'raises when scaling down number of instances but remaining above quota' do + process.update(state: 'STARTED') + org.quota_definition = QuotaDefinition.make(memory_limit: 32) act_as_cf_admin { org.save } @@ -487,6 +547,7 @@ def act_as_cf_admin end it 'allows stopping an app that is above quota' do + process.update(state: 'STARTED') org.quota_definition = QuotaDefinition.make(memory_limit: 72) act_as_cf_admin { org.save } @@ -503,6 +564,7 @@ def act_as_cf_admin act_as_cf_admin { org.save } process.memory = 40 + process.state = 'STARTED' expect { process.save }.to raise_error(Sequel::ValidationFailed, /quota_exceeded/) process.memory = 32 @@ -565,6 +627,7 @@ def act_as_cf_admin :health_check_timeout, :health_check_type, :instances, + :log_rate_limit, :memory, :name, :package_state, @@ -598,6 +661,7 @@ def act_as_cf_admin :health_check_timeout, :health_check_type, :instances, + :log_rate_limit, :memory, :name, :production, @@ -1333,6 +1397,22 @@ def act_as_cf_admin end end + describe 'default log_rate_limit' do + before do + TestConfig.override(default_app_log_rate_limit_in_bytes_per_second: 1024) + end + + it 'should use the provided quota' do + process = ProcessModel.make(log_rate_limit: 256) + expect(process.log_rate_limit).to eq(256) + end + + it 'should use the default quota' do + process = ProcessModel.make + expect(process.log_rate_limit).to eq(1024) + end + end + describe 'instance_file_descriptor_limit' do before do TestConfig.override(instance_file_descriptor_limit: 200) diff --git a/spec/unit/models/runtime/quota_definition_spec.rb b/spec/unit/models/runtime/quota_definition_spec.rb index a886605b92d..7c690a4d27d 100644 --- a/spec/unit/models/runtime/quota_definition_spec.rb +++ b/spec/unit/models/runtime/quota_definition_spec.rb @@ -110,6 +110,15 @@ module VCAP::CloudController expect(quota_definition).to be_valid end + it 'log_rate_limit cannot be less than -1 (unlimited)' do + quota_definition.log_rate_limit = -2 + expect(quota_definition).not_to be_valid + expect(quota_definition.errors.on(:log_rate_limit)).to include(:invalid_log_rate_limit) + + quota_definition.log_rate_limit = -1 + expect(quota_definition).to be_valid + end + it 'app_task_limit cannot be less than -1 ("unlimited")' do quota_definition.app_task_limit = -2 expect(quota_definition).not_to be_valid @@ -133,12 +142,14 @@ module VCAP::CloudController it { is_expected.to export_attributes :name, :non_basic_services_allowed, :total_services, :total_routes, :total_private_domains, :memory_limit, :trial_db_allowed, :instance_memory_limit, - :app_instance_limit, :app_task_limit, :total_service_keys, :total_reserved_route_ports + :app_instance_limit, :app_task_limit, :total_service_keys, :total_reserved_route_ports, + :log_rate_limit } it { is_expected.to import_attributes :name, :non_basic_services_allowed, :total_services, :total_routes, :total_private_domains, :memory_limit, :trial_db_allowed, :instance_memory_limit, - :app_instance_limit, :app_task_limit, :total_service_keys, :total_reserved_route_ports + :app_instance_limit, :app_task_limit, :total_service_keys, :total_reserved_route_ports, + :log_rate_limit } end diff --git a/spec/unit/models/runtime/space_quota_definition_spec.rb b/spec/unit/models/runtime/space_quota_definition_spec.rb index c5438e62fe2..73d986b5632 100644 --- a/spec/unit/models/runtime/space_quota_definition_spec.rb +++ b/spec/unit/models/runtime/space_quota_definition_spec.rb @@ -68,6 +68,17 @@ module VCAP::CloudController end end + describe 'log_rate_limit' do + it 'cannot be less than -1' do + space_quota_definition.log_rate_limit = -2 + expect(space_quota_definition).not_to be_valid + expect(space_quota_definition.errors.on(:log_rate_limit)).to include(:invalid_log_rate_limit) + + space_quota_definition.log_rate_limit = -1 + expect(space_quota_definition).to be_valid + end + end + describe 'total_reserved_route_ports' do let(:err_msg) do 'Total reserved ports must be -1, 0, or a positive integer, must ' \ @@ -163,13 +174,13 @@ module VCAP::CloudController it do is_expected.to export_attributes :name, :organization_guid, :non_basic_services_allowed, :total_services, :total_routes, :memory_limit, :instance_memory_limit, :app_instance_limit, :app_task_limit, - :total_service_keys, :total_reserved_route_ports + :total_service_keys, :total_reserved_route_ports, :log_rate_limit end it do is_expected.to import_attributes :name, :organization_guid, :non_basic_services_allowed, :total_services, :total_routes, :memory_limit, :instance_memory_limit, :app_instance_limit, :app_task_limit, - :total_service_keys, :total_reserved_route_ports + :total_service_keys, :total_reserved_route_ports, :log_rate_limit end end diff --git a/spec/unit/models/runtime/space_spec.rb b/spec/unit/models/runtime/space_spec.rb index 2a0812dd497..ebb4f77f504 100644 --- a/spec/unit/models/runtime/space_spec.rb +++ b/spec/unit/models/runtime/space_spec.rb @@ -665,6 +665,57 @@ module VCAP::CloudController end end + describe '#has_remaining_log_rate_limit' do + let(:log_rate_limit) { 10 } + let(:quota) { SpaceQuotaDefinition.make(log_rate_limit: log_rate_limit, organization: org) } + let(:org) { Organization.make } + let(:space) { Space.make(organization: org, space_quota_definition: quota) } + let(:space2) { Space.make(organization: org, space_quota_definition: quota) } + let!(:app_model) { AppModel.make(space: space) } + + context 'when the quota is unlimited' do + let(:log_rate_limit) { QuotaDefinition::UNLIMITED } + + it 'handles large log quotas' do + expect(space.has_remaining_log_rate_limit(10_000_000)).to be_truthy + end + end + + context 'when nothing is running' do + it 'uses the log_rate_limit' do + expect(space.has_remaining_log_rate_limit(10)).to be_truthy + expect(space.has_remaining_log_rate_limit(11)).to be_falsey + end + end + + context 'when something else is running' do + it 'takes all things in the space into account' do + ProcessModelFactory.make(space: space, log_rate_limit: 5, state: 'STARTED') + expect(space.has_remaining_log_rate_limit(5)).to be_truthy + expect(space.has_remaining_log_rate_limit(6)).to be_falsey + + ProcessModelFactory.make(space: space, log_rate_limit: 1, state: 'STARTED') + expect(space.has_remaining_log_rate_limit(4)).to be_truthy + expect(space.has_remaining_log_rate_limit(5)).to be_falsey + + TaskModel.make(app: app_model, log_rate_limit: 1, state: TaskModel::RUNNING_STATE) + expect(space.has_remaining_log_rate_limit(3)).to be_truthy + expect(space.has_remaining_log_rate_limit(4)).to be_falsey + end + + context 'when processes are running in another space' do + it 'only accounts for processes running in the owning space' do + ProcessModelFactory.make(space: space2, log_rate_limit: 1, instances: 2, state: 'STARTED') + + expect(space.has_remaining_log_rate_limit(10)).to be_truthy + expect(space.has_remaining_log_rate_limit(11)).to be_falsey + expect(space2.has_remaining_log_rate_limit(8)).to be_truthy + expect(space2.has_remaining_log_rate_limit(9)).to be_falsey + end + end + end + end + describe '#instance_memory_limit' do let(:org) { Organization.make } let(:space_quota) { SpaceQuotaDefinition.make(instance_memory_limit: 50, organization: org) } diff --git a/spec/unit/models/runtime/task_model_spec.rb b/spec/unit/models/runtime/task_model_spec.rb index c9482fda8bd..deb8af74bfa 100644 --- a/spec/unit/models/runtime/task_model_spec.rb +++ b/spec/unit/models/runtime/task_model_spec.rb @@ -289,6 +289,15 @@ module VCAP::CloudController end describe 'quotas' do + it 'errors when log_rate_limit is below -1' do + expect { + TaskModel.make( + log_rate_limit: -2, + app: app, + ) + }.to raise_error + end + describe 'space quotas' do let(:space) { Space.make organization: org, space_quota_definition: quota } @@ -305,6 +314,59 @@ module VCAP::CloudController end end + describe 'when the log rate limit quota is unlimited' do + let(:quota) { SpaceQuotaDefinition.make(log_rate_limit: -1, organization: org) } + + it 'allows tasks to run with unlimited rate limits' do + expect { + TaskModel.make( + log_rate_limit: -1, + app: app, + ) + }.not_to raise_error + end + + it 'allows tasks to run with rate limits' do + expect { + TaskModel.make( + log_rate_limit: 1_000_000_000_000, + app: app, + ) + }.not_to raise_error + end + end + + describe 'when the quota has a log_rate_limit' do + let(:quota) { SpaceQuotaDefinition.make(log_rate_limit: 200, organization: org) } + + it 'allows tasks that fit in the available log rate' do + expect { + TaskModel.make( + log_rate_limit: 100, + app: app, + ) + }.not_to raise_error + end + + it 'raises an error if the task does not fit in the remaining space' do + expect { + TaskModel.make( + log_rate_limit: 201, + app: app, + ) + }.to raise_error Sequel::ValidationFailed, 'log_rate_limit exceeds space log rate quota' + end + + it 'raises an error if the task has an unlimited rate limit' do + expect { + TaskModel.make( + log_rate_limit: -1, + app: app, + ) + }.to raise_error Sequel::ValidationFailed, "log_rate_limit cannot be unlimited in space '#{space.name}'." + end + end + describe 'when the quota has a memory_limit' do let(:quota) { SpaceQuotaDefinition.make(memory_limit: 20, organization: org) } @@ -407,12 +469,35 @@ module VCAP::CloudController expect { TaskModel.make( memory_in_mb: 21, + log_rate_limit: 21_000, app: app, ) }.not_to raise_error end end + describe 'when the quota has a log_rate_limit' do + let(:quota) { QuotaDefinition.make(log_rate_limit: 200) } + + it 'does allow a task that fits in the limit to start' do + expect { + TaskModel.make( + log_rate_limit: 199, + app: app, + ) + }.to_not raise_error + end + + it 'does not allow a task that exceeds the limit to start' do + expect { + TaskModel.make( + log_rate_limit: 10_000, + app: app, + ) + }.to raise_error /log_rate_limit exceeds organization log rate/ + end + end + describe 'when the quota has a memory_limit' do let(:quota) { QuotaDefinition.make(memory_limit: 20) } diff --git a/spec/unit/presenters/v2/process_model_presenter_spec.rb b/spec/unit/presenters/v2/process_model_presenter_spec.rb index d1c03c13256..9ff7ebcbb8b 100644 --- a/spec/unit/presenters/v2/process_model_presenter_spec.rb +++ b/spec/unit/presenters/v2/process_model_presenter_spec.rb @@ -69,6 +69,7 @@ module CloudController::Presenters::V2 'memory' => 1024, 'instances' => 1, 'disk_quota' => 1024, + 'log_rate_limit' => 1_048_576, 'state' => 'STOPPED', 'version' => process.version, 'command' => 'start', diff --git a/spec/unit/presenters/v3/app_manifest_presenter_spec.rb b/spec/unit/presenters/v3/app_manifest_presenter_spec.rb index e67b1d71e00..8d76975cc81 100644 --- a/spec/unit/presenters/v3/app_manifest_presenter_spec.rb +++ b/spec/unit/presenters/v3/app_manifest_presenter_spec.rb @@ -78,6 +78,7 @@ module VCAP::CloudController::Presenters::V3 health_check_type: 'http', health_check_http_endpoint: '/foobar', health_check_timeout: 5, + log_rate_limit: 1_048_576, command: 'Do it now!', type: 'aaaaa', ) @@ -119,6 +120,7 @@ module VCAP::CloudController::Presenters::V3 'type' => process1.type, 'instances' => process1.instances, 'memory' => "#{process1.memory}M", + 'log_rate_limit_per_second' => '1M', 'disk_quota' => "#{process1.disk_quota}M", 'command' => process1.command, 'health-check-type' => process1.health_check_type, @@ -128,6 +130,7 @@ module VCAP::CloudController::Presenters::V3 { 'type' => process2.type, 'instances' => process2.instances, + 'log_rate_limit_per_second' => '1M', 'memory' => "#{process2.memory}M", 'disk_quota' => "#{process2.disk_quota}M", 'health-check-type' => process2.health_check_type, @@ -166,6 +169,7 @@ module VCAP::CloudController::Presenters::V3 { 'type' => process1.type, 'instances' => process1.instances, + 'log_rate_limit_per_second' => '1M', 'memory' => "#{process1.memory}M", 'disk_quota' => "#{process1.disk_quota}M", 'health-check-type' => process1.health_check_type, @@ -173,6 +177,7 @@ module VCAP::CloudController::Presenters::V3 { 'type' => process2.type, 'instances' => process2.instances, + 'log_rate_limit_per_second' => '1M', 'memory' => "#{process2.memory}M", 'disk_quota' => "#{process2.disk_quota}M", 'health-check-type' => process2.health_check_type, diff --git a/spec/unit/presenters/v3/app_manifest_presenters/process_properties_presenter_spec.rb b/spec/unit/presenters/v3/app_manifest_presenters/process_properties_presenter_spec.rb index e4684dd1fe6..c75f3c17cd6 100644 --- a/spec/unit/presenters/v3/app_manifest_presenters/process_properties_presenter_spec.rb +++ b/spec/unit/presenters/v3/app_manifest_presenters/process_properties_presenter_spec.rb @@ -32,5 +32,31 @@ module VCAP::CloudController::Presenters::V3::AppManifestPresenters end end end + + describe '#process_hash' do + let(:process) { VCAP::CloudController::ProcessModel.make } + + it 'renders a compact hash of the process' do + hash = subject.process_hash(process) + expect(hash).to eq({ + 'type' => 'web', + 'instances' => 1, + 'memory' => '1024M', + 'disk_quota' => '1024M', + 'log_rate_limit_per_second' => '1M', + 'health-check-type' => 'port', + }) + end + end + + describe '#add_units_log_rate_limit' do + it 'is consistant with other quotas with output' do + expect(subject.add_units_log_rate_limit(-1)).to eq('-1B') + expect(subject.add_units_log_rate_limit(256)).to eq('256B') + expect(subject.add_units_log_rate_limit(2_048)).to eq('2K') + expect(subject.add_units_log_rate_limit(4_194_304)).to eq('4M') + expect(subject.add_units_log_rate_limit(6_442_450_944)).to eq('6G') + end + end end end diff --git a/spec/unit/presenters/v3/process_presenter_spec.rb b/spec/unit/presenters/v3/process_presenter_spec.rb index 09894f8c890..affccd4c66b 100644 --- a/spec/unit/presenters/v3/process_presenter_spec.rb +++ b/spec/unit/presenters/v3/process_presenter_spec.rb @@ -147,6 +147,15 @@ module VCAP::CloudController::Presenters::V3 expect(result[:command]).to eq('[PRIVATE DATA HIDDEN]') end end + + context 'log quota is -1' do + before do + process.log_rate_limit = -1 + end + it 'displays it as unlimited' do + expect(result[:log_rate_limit_in_bytes_per_second]).to eq(-1) + end + end end end end diff --git a/spec/unit/presenters/v3/process_stats_presenter_spec.rb b/spec/unit/presenters/v3/process_stats_presenter_spec.rb index 53df8e43f12..1599806f5ce 100644 --- a/spec/unit/presenters/v3/process_stats_presenter_spec.rb +++ b/spec/unit/presenters/v3/process_stats_presenter_spec.rb @@ -66,12 +66,14 @@ module VCAP::CloudController::Presenters::V3 uptime: 12345, mem_quota: process[:memory] * 1024 * 1024, disk_quota: process[:disk_quota] * 1024 * 1024, + log_rate_limit: process[:log_rate_limit], fds_quota: process.file_descriptors, usage: { time: '2015-12-08 16:54:48 -0800', cpu: 80, mem: 128, disk: 1024, + log_rate: 2048, } } }, @@ -86,12 +88,14 @@ module VCAP::CloudController::Presenters::V3 uptime: 42, mem_quota: process[:memory] * 1024 * 1024, disk_quota: process[:disk_quota] * 1024 * 1024, + log_rate_limit: process[:log_rate_limit], fds_quota: process.file_descriptors, usage: { time: '2015-03-13 16:54:48 -0800', cpu: 70, mem: 128, disk: 1024, + log_rate: 7168, } } }, @@ -116,11 +120,13 @@ module VCAP::CloudController::Presenters::V3 expect(result[0][:uptime]).to eq(12345) expect(result[0][:mem_quota]).to eq(process[:memory] * 1024 * 1024) expect(result[0][:disk_quota]).to eq(process[:disk_quota] * 1024 * 1024) + expect(result[0][:log_rate_limit]).to eq(process[:log_rate_limit]) expect(result[0][:fds_quota]).to eq(process.file_descriptors) expect(result[0][:usage]).to eq({ time: '2015-12-08 16:54:48 -0800', cpu: 80, mem: 128, - disk: 1024 }) + disk: 1024, + log_rate: 2048 }) expect(result[1][:type]).to eq(process.type) expect(result[1][:index]).to eq(1) @@ -133,7 +139,8 @@ module VCAP::CloudController::Presenters::V3 expect(result[1][:usage]).to eq({ time: '2015-03-13 16:54:48 -0800', cpu: 70, mem: 128, - disk: 1024 }) + disk: 1024, + log_rate: 7168 }) expect(result[2]).to eq( type: process.type, @@ -190,11 +197,13 @@ module VCAP::CloudController::Presenters::V3 expect(result[0][:uptime]).to eq(12345) expect(result[0][:mem_quota]).to eq(process[:memory] * 1024 * 1024) expect(result[0][:disk_quota]).to eq(process[:disk_quota] * 1024 * 1024) + expect(result[0][:log_rate_limit]).to eq(process[:log_rate_limit]) expect(result[0][:fds_quota]).to eq(process.file_descriptors) expect(result[0][:usage]).to eq({ time: '2015-12-08 16:54:48 -0800', cpu: 80, mem: 128, - disk: 1024 }) + disk: 1024, + log_rate: 2048 }) end end @@ -230,6 +239,7 @@ module VCAP::CloudController::Presenters::V3 expect(result[0][:uptime]).to eq(12345) expect(result[0][:mem_quota]).to be_nil expect(result[0][:disk_quota]).to be_nil + expect(result[0][:log_rate_limit]).to be_nil expect(result[0][:fds_quota]).to eq(process.file_descriptors) expect(result[0][:usage]).to eq({}) end diff --git a/spec/unit/presenters/v3/space_quota_presenter_spec.rb b/spec/unit/presenters/v3/space_quota_presenter_spec.rb index 20b3cfddac1..2229ff81240 100644 --- a/spec/unit/presenters/v3/space_quota_presenter_spec.rb +++ b/spec/unit/presenters/v3/space_quota_presenter_spec.rb @@ -17,6 +17,7 @@ module VCAP::CloudController::Presenters::V3 instance_memory_limit: 3, app_instance_limit: 4, app_task_limit: 5, + log_rate_limit: 2000, non_basic_services_allowed: false, total_services: 6, total_service_keys: 7, @@ -42,6 +43,7 @@ module VCAP::CloudController::Presenters::V3 expect(result[:apps][:per_process_memory_in_mb]).to eq(space_quota.instance_memory_limit) expect(result[:apps][:total_instances]).to eq(space_quota.app_instance_limit) expect(result[:apps][:per_app_tasks]).to eq(space_quota.app_task_limit) + expect(result[:apps][:log_rate_limit_in_bytes_per_second]).to eq(space_quota.log_rate_limit) expect(result[:services][:paid_services_allowed]).to eq(space_quota.non_basic_services_allowed) expect(result[:services][:total_service_instances]).to eq(space_quota.total_services) expect(result[:services][:total_service_keys]).to eq(space_quota.total_service_keys) @@ -67,6 +69,7 @@ module VCAP::CloudController::Presenters::V3 instance_memory_limit: -1, app_instance_limit: -1, app_task_limit: -1, + log_rate_limit: -1, total_services: -1, total_service_keys: -1, total_routes: -1, @@ -79,6 +82,7 @@ module VCAP::CloudController::Presenters::V3 expect(result[:apps][:per_process_memory_in_mb]).to be_nil expect(result[:apps][:total_instances]).to be_nil expect(result[:apps][:per_app_tasks]).to be_nil + expect(result[:apps][:log_rate_limit_in_bytes_per_second]).to be_nil expect(result[:services][:total_service_instances]).to be_nil expect(result[:services][:total_service_keys]).to be_nil expect(result[:routes][:total_routes]).to be_nil diff --git a/spec/unit/presenters/v3/task_presenter_spec.rb b/spec/unit/presenters/v3/task_presenter_spec.rb index 4befd87aa89..a1db39405a7 100644 --- a/spec/unit/presenters/v3/task_presenter_spec.rb +++ b/spec/unit/presenters/v3/task_presenter_spec.rb @@ -9,6 +9,7 @@ module VCAP::CloudController::Presenters::V3 failure_reason: 'sup dawg', memory_in_mb: 2048, disk_in_mb: 4048, + log_rate_limit: 1024, created_at: Time.at(1), sequence_id: 5 ) @@ -64,6 +65,7 @@ module VCAP::CloudController::Presenters::V3 expect(result[:result][:failure_reason]).to eq 'sup dawg' expect(result[:memory_in_mb]).to eq(task.memory_in_mb) expect(result[:disk_in_mb]).to eq(task.disk_in_mb) + expect(result[:log_rate_limit_in_bytes_per_second]).to eq(task.log_rate_limit) expect(result[:sequence_id]).to eq(5) expect(result[:created_at]).to eq(task.created_at.iso8601) expect(result[:updated_at]).to eq(task.updated_at.iso8601)