diff --git a/lib/gooddata/models/user_filters/user_filter_builder.rb b/lib/gooddata/models/user_filters/user_filter_builder.rb index ac139bb81..e0f42838d 100644 --- a/lib/gooddata/models/user_filters/user_filter_builder.rb +++ b/lib/gooddata/models/user_filters/user_filter_builder.rb @@ -22,6 +22,19 @@ module UserFilterBuilder @all_domain_users = {} @mutex = Mutex.new + # Caps on error entries embedded in a raised exception's message (and logged). + # Domain-wide syncs can produce one entry per user/value across a whole domain, + # and a single entry can embed large value lists; stringifying the raw + # collection exhausts the JRuby heap (GRIF-951). + MAX_ERRORS_IN_MESSAGE = 10 + MAX_ERROR_ENTRY_CHARS = 1_000 + + # Renders a bounded sample of an error collection for logs and exception + # messages: caps both the number of entries and each entry's rendered size. + def self.bounded_error_sample(errors) + errors.take(MAX_ERRORS_IN_MESSAGE).map { |e| e.inspect.slice(0, MAX_ERROR_ENTRY_CHARS) } + end + # Main Entry function. Gets values and processes them to get filters # that are suitable for other function to process. # Values can be read from file or provided inline as an array. @@ -488,7 +501,9 @@ def self.execute_mufs(user_filters, options = {}) errors = errors.map do |e| e.merge(pid: project.pid) end - fail GoodData::FilterMaqlizationError, errors + sample = bounded_error_sample(errors) + GoodData.logger.error("Maqlizing MUFs failed with #{errors.size} error(s). First #{sample.size}: #{sample.pretty_inspect}") + fail GoodData::FilterMaqlizationError, "Maqlizing MUFs resulted in #{errors.size} error(s). First #{sample.size}: #{sample}" end filters = user_filters.map { |data| client.create(MandatoryUserFilter, data, project: project) } @@ -542,7 +557,11 @@ def self.execute_mufs(user_filters, options = {}) end project_log_formatter.log_user_filter_results(create_results, to_create) create_errors = create_results.select { |r| r[:status] == :failed } - fail "Creating MUFs resulted in errors: #{create_errors}" if create_errors.any? + if create_errors.any? + sample = bounded_error_sample(create_errors) + GoodData.logger.error("Creating MUFs failed with #{create_errors.size} error(s). First #{sample.size}: #{sample.pretty_inspect}") + fail "Creating MUFs resulted in errors, count: #{create_errors.size}, first #{sample.size}: #{sample}" + end end if to_delete.empty? @@ -575,7 +594,11 @@ def self.execute_mufs(user_filters, options = {}) project_log_formatter.log_user_filter_results(delete_results, to_delete) delete_errors = delete_results.select { |r| r[:status] == :failed } if delete_results - fail "Deleting MUFs resulted in errors: #{delete_errors}" if delete_errors&.any? + if delete_errors&.any? + sample = bounded_error_sample(delete_errors) + GoodData.logger.error("Deleting MUFs failed with #{delete_errors.size} error(s). First #{sample.size}: #{sample.pretty_inspect}") + fail "Deleting MUFs resulted in errors, count: #{delete_errors.size}, first #{sample.size}: #{sample}" + end end end @@ -643,7 +666,11 @@ def self.execute(user_filters, project_filters, klass, options = {}) maqlify_filters(filters, users, options.merge(users_cache: users_cache, users_must_exist: users_must_exist)) end - fail GoodData::FilterMaqlizationError, errors if !ignore_missing_values && !errors.empty? + if !ignore_missing_values && !errors.empty? + sample = bounded_error_sample(errors) + GoodData.logger.error("Maqlizing variables failed with #{errors.size} error(s). First #{sample.size}: #{sample.pretty_inspect}") + fail GoodData::FilterMaqlizationError, "Maqlizing variables resulted in #{errors.size} error(s). First #{sample.size}: #{sample}" + end filters = user_filters.map { |data| client.create(klass, data, project: project) } resolve_user_filters(filters, project_filters) end diff --git a/spec/unit/models/user_filters/user_filter_builder_spec.rb b/spec/unit/models/user_filters/user_filter_builder_spec.rb index 3452b66ac..cf3b975dc 100644 --- a/spec/unit/models/user_filters/user_filter_builder_spec.rb +++ b/spec/unit/models/user_filters/user_filter_builder_spec.rb @@ -199,5 +199,26 @@ expect { subject.execute_mufs(filter_definitions, options) }.to raise_error(/Creating MUFs resulted in errors/) end end + + context 'when creating MUFs results in a large number of errors' do + let(:failed_users) do + Array.new(500) { |i| { 'login' => "user#{i}@example.com", 'detail' => 'x' * 1_000 } } + end + + before do + allow(client).to receive(:post) + .and_return 'userFiltersUpdateResult' => { 'failed' => failed_users } + end + + it 'raises an error with a message bounded to the first few errors' do + expect { subject.execute_mufs(filter_definitions, options) }.to raise_error do |error| + expect(error.message).to match(/Creating MUFs resulted in errors, count: 500, first 10/) + expect(error.message).to include('user0@example.com') + expect(error.message).to include('user9@example.com') + expect(error.message).not_to include('user10@example.com') + expect(error.message.size).to be < 15_000 + end + end + end end end