diff --git a/ee/app/graphql/resolvers/ci/runner_usage_by_project_resolver.rb b/ee/app/graphql/resolvers/ci/runner_usage_by_project_resolver.rb index 138b06db8ff3bef2f125d20ff59949c9848b49df..c1ff1fac19c8318d6603e133e42c8004fc5ab3f6 100644 --- a/ee/app/graphql/resolvers/ci/runner_usage_by_project_resolver.rb +++ b/ee/app/graphql/resolvers/ci/runner_usage_by_project_resolver.rb @@ -44,11 +44,12 @@ def resolve(from_date: nil, to_date: nil, runner_type: nil, projects_limit: nil) "'to_date' must be greater than 'from_date' and be within 1 year" end - result = ::Ci::Runners::GetUsageByProjectService.new(current_user, + result = ::Ci::Runners::GetUsageByProjectService.new( + current_user, runner_type: runner_type, from_date: from_date, to_date: to_date, - max_project_count: [MAX_PROJECTS_LIMIT, projects_limit || DEFAULT_PROJECTS_LIMIT].min + max_item_count: [MAX_PROJECTS_LIMIT, projects_limit || DEFAULT_PROJECTS_LIMIT].min ).execute raise Gitlab::Graphql::Errors::ArgumentError, result.message if result.error? @@ -61,7 +62,7 @@ def resolve(from_date: nil, to_date: nil, runner_type: nil, projects_limit: nil) def prepare_result(payload) payload.map do |project_usage| { - project_id: project_usage['grouped_project_id'], + project_id: project_usage['project_id_bucket'], ci_minutes_used: project_usage['total_duration_in_mins'], ci_build_count: project_usage['count_builds'] } diff --git a/ee/app/graphql/resolvers/ci/runner_usage_resolver.rb b/ee/app/graphql/resolvers/ci/runner_usage_resolver.rb index 9f44b56c069c7f7a1b810e22d7f3f5f1e5c4c90c..83c4867be27004a33a7ee71d3adc45f9259a112c 100644 --- a/ee/app/graphql/resolvers/ci/runner_usage_resolver.rb +++ b/ee/app/graphql/resolvers/ci/runner_usage_resolver.rb @@ -49,7 +49,7 @@ def resolve(from_date: nil, to_date: nil, runner_type: nil, runners_limit: nil) runner_type: runner_type, from_date: from_date, to_date: to_date, - max_runners_count: [MAX_RUNNERS_LIMIT, runners_limit || DEFAULT_RUNNERS_LIMIT].min + max_item_count: [MAX_RUNNERS_LIMIT, runners_limit || DEFAULT_RUNNERS_LIMIT].min ).execute raise_resource_not_available_error!(result.message) if result.error? @@ -62,7 +62,7 @@ def resolve(from_date: nil, to_date: nil, runner_type: nil, runners_limit: nil) def prepare_result(payload) payload.map do |runner_usage| { - runner_id: runner_usage['runner_id'], + runner_id: runner_usage['runner_id_bucket'], ci_minutes_used: runner_usage['total_duration_in_mins'], ci_build_count: runner_usage['count_builds'] } diff --git a/ee/app/services/ci/runners/generate_usage_csv_service.rb b/ee/app/services/ci/runners/generate_usage_csv_service.rb index 79f82b382c4a4e05b97195d0f0c120a6352efb4e..04eeee4b86ff954852aeecf614c9dd0981950315 100644 --- a/ee/app/services/ci/runners/generate_usage_csv_service.rb +++ b/ee/app/services/ci/runners/generate_usage_csv_service.rb @@ -27,8 +27,8 @@ def initialize(current_user, runner_type:, from_date:, to_date:, max_project_cou def execute result = ::Ci::Runners::GetUsageByProjectService.new(current_user, runner_type: runner_type, - from_date: from_date, to_date: to_date, max_project_count: max_project_count, - group_by_columns: [:status, :runner_type]).execute + from_date: from_date, to_date: to_date, max_item_count: max_project_count, + additional_group_by_columns: %i[status runner_type]).execute return result unless result.success? @@ -39,7 +39,7 @@ def execute # rubocop: disable CodeReuse/ActiveRecord -- This is an enumerable # rubocop: disable Database/AvoidUsingPluckWithoutLimit -- This is an enumerable - export_status[:projects_written] = rows.pluck('grouped_project_id').compact.sort.uniq.count + export_status[:projects_written] = rows.pluck('project_id_bucket').compact.sort.uniq.count # rubocop: enable Database/AvoidUsingPluckWithoutLimit # rubocop: enable CodeReuse/ActiveRecord export_status[:projects_expected] = @@ -58,7 +58,7 @@ def execute def header_to_value_hash { - 'Project ID' => 'grouped_project_id', + 'Project ID' => 'project_id_bucket', 'Project path' => 'project_path', 'Status' => 'status', 'Runner type' => 'runner_type', @@ -70,7 +70,7 @@ def header_to_value_hash def transform_rows(result) # rubocop: disable CodeReuse/ActiveRecord -- This is a ClickHouse query - ids = result.pluck('grouped_project_id') # rubocop: disable Database/AvoidUsingPluckWithoutLimit -- The limit is already implemented in the ClickHouse query + ids = result.pluck('project_id_bucket') # rubocop: disable Database/AvoidUsingPluckWithoutLimit -- The limit is already implemented in the ClickHouse query # rubocop: enable CodeReuse/ActiveRecord return result if ids.empty? @@ -80,7 +80,7 @@ def transform_rows(result) runner_types_by_value = Ci::Runner.runner_types.to_h.invert # Annotate rows with project paths, human-readable durations, etc. result.each do |row| - row['project_path'] = projects[row['grouped_project_id']&.to_i] + row['project_path'] = projects[row['project_id_bucket']&.to_i] row['runner_type'] = runner_types_by_value[row['runner_type']&.to_i] row['total_duration_human_readable'] = ActiveSupport::Duration.build(row['total_duration_in_mins'] * 60).inspect diff --git a/ee/app/services/ci/runners/get_usage_by_project_service.rb b/ee/app/services/ci/runners/get_usage_by_project_service.rb index 0ca651113e6699972c32f65a75ad29690635d814..044cdde30a902a29ab49b097eb3e94b7cdfafd43 100644 --- a/ee/app/services/ci/runners/get_usage_by_project_service.rb +++ b/ee/app/services/ci/runners/get_usage_by_project_service.rb @@ -2,98 +2,19 @@ module Ci module Runners - class GetUsageByProjectService - include Gitlab::Utils::StrongMemoize - def initialize(current_user, runner_type:, from_date:, to_date:, max_project_count:, group_by_columns: []) - @current_user = current_user - @runner_type = Ci::Runner.runner_types[runner_type] - @from_date = from_date - @to_date = to_date - @max_project_count = max_project_count - @group_by_columns = group_by_columns - end - - def execute - unless ::Gitlab::ClickHouse.configured? - return ServiceResponse.error(message: 'ClickHouse database is not configured', - reason: :db_not_configured) - end - - unless Ability.allowed?(@current_user, :read_runner_usage) - return ServiceResponse.error(message: 'Insufficient permissions', - reason: :insufficient_permissions) - end - - data = ClickHouse::Client.select(clickhouse_query, :main) - ServiceResponse.success(payload: data) - end + class GetUsageByProjectService < GetUsageServiceBase + extend ::Gitlab::Utils::Override private - attr_reader :runner_type, :from_date, :to_date, :group_by_columns, :max_project_count - - def clickhouse_query - grouping_columns = ['grouped_project_id', *group_by_columns].join(', ') - raw_query = <<~SQL.squish - WITH top_projects AS - ( - SELECT project_id - FROM ci_used_minutes_mv - WHERE #{where_conditions} - GROUP BY project_id - ORDER BY sumSimpleState(total_duration) DESC - LIMIT {max_project_count: UInt64} - ) - SELECT IF(project_id IN top_projects, project_id, NULL) AS grouped_project_id, #{select_list} - FROM ci_used_minutes_mv - WHERE #{where_conditions} - GROUP BY #{grouping_columns} - ORDER BY (grouped_project_id IS NULL), #{order_list} - SQL - - ClickHouse::Client::Query.new(raw_query: raw_query, placeholders: placeholders) - end - - def placeholders - placeholders = { - runner_type: runner_type, - from_date: format_date(from_date), - to_date: format_date(to_date + 1), # Include jobs until the end of the day - max_project_count: max_project_count - } - - placeholders.compact - end - - def select_list - [ - *group_by_columns, - 'countMerge(count_builds) AS count_builds', - 'toUInt64(sumSimpleState(total_duration) / 60000) AS total_duration_in_mins' - ].join(', ') - end - strong_memoize_attr :select_list - - def order_list - [ - 'total_duration_in_mins DESC', - 'grouped_project_id ASC', - *group_by_columns.map { |column| "#{column} ASC" } - ].join(', ') - end - strong_memoize_attr :order_list - - def where_conditions - <<~SQL - #{'runner_type = {runner_type: UInt8} AND' if runner_type} - finished_at_bucket >= {from_date: DateTime('UTC', 6)} AND - finished_at_bucket < {to_date: DateTime('UTC', 6)} - SQL + override :table_name + def table_name + 'ci_used_minutes' end - strong_memoize_attr :where_conditions - def format_date(date) - date.strftime('%Y-%m-%d %H:%M:%S') + override :bucket_column + def bucket_column + 'project_id' end end end diff --git a/ee/app/services/ci/runners/get_usage_service.rb b/ee/app/services/ci/runners/get_usage_service.rb index c1af8dc2c56d6c3053de3e934618bd99ba9f0867..4916acd4e3888f724ad4beea25c093aec244d3a6 100644 --- a/ee/app/services/ci/runners/get_usage_service.rb +++ b/ee/app/services/ci/runners/get_usage_service.rb @@ -2,82 +2,19 @@ module Ci module Runners - class GetUsageService - include Gitlab::Utils::StrongMemoize - def initialize(current_user, runner_type:, from_date:, to_date:, max_runners_count:) - @current_user = current_user - @runner_type = Ci::Runner.runner_types[runner_type] - @from_date = from_date - @to_date = to_date - @max_runners_count = max_runners_count - end - - def execute - unless ::Gitlab::ClickHouse.configured? - return ServiceResponse.error(message: 'ClickHouse database is not configured', - reason: :db_not_configured) - end - - unless Ability.allowed?(@current_user, :read_runner_usage) - return ServiceResponse.error(message: 'Insufficient permissions', - reason: :insufficient_permissions) - end - - data = ClickHouse::Client.select(clickhouse_query, :main) - ServiceResponse.success(payload: data) - end + class GetUsageService < GetUsageServiceBase + extend ::Gitlab::Utils::Override private - attr_reader :runner_type, :from_date, :to_date, :max_runners_count - - def clickhouse_query - raw_query = <<~SQL.squish - WITH top_runners AS - ( - SELECT runner_id - FROM ci_used_minutes_by_runner_daily - WHERE #{where_conditions} - GROUP BY runner_id - ORDER BY sumSimpleState(total_duration) DESC - LIMIT {max_runners_count: UInt64} - ) - SELECT - IF(ci_used_minutes_by_runner_daily.runner_id IN top_runners, ci_used_minutes_by_runner_daily.runner_id, NULL) - AS runner_id, - countMerge(count_builds) AS count_builds, - toUInt64(sumSimpleState(total_duration) / 60000) AS total_duration_in_mins - FROM ci_used_minutes_by_runner_daily - WHERE #{where_conditions} - GROUP BY runner_id - ORDER BY (runner_id IS NULL), total_duration_in_mins DESC, runner_id ASC - SQL - - ClickHouse::Client::Query.new(raw_query: raw_query, placeholders: placeholders) - end - - def placeholders - placeholders = { - runner_type: runner_type, - from_date: format_date(from_date), - to_date: format_date(to_date + 1), # Include jobs until the end of the day - max_runners_count: max_runners_count - } - - placeholders.compact - end - - def where_conditions - <<~SQL - #{'runner_type = {runner_type: UInt8} AND' if runner_type} - finished_at_bucket >= {from_date: DateTime('UTC', 6)} AND - finished_at_bucket < {to_date: DateTime('UTC', 6)} - SQL + override :table_name + def table_name + 'ci_used_minutes_by_runner_daily' end - strong_memoize_attr :where_conditions - def format_date(date) - date.strftime('%Y-%m-%d %H:%M:%S') + override :bucket_column + def bucket_column + 'runner_id' end end end diff --git a/ee/app/services/ci/runners/get_usage_service_base.rb b/ee/app/services/ci/runners/get_usage_service_base.rb new file mode 100644 index 0000000000000000000000000000000000000000..29182e52e7152f46d0e9ee237964e7806760e158 --- /dev/null +++ b/ee/app/services/ci/runners/get_usage_service_base.rb @@ -0,0 +1,110 @@ +# frozen_string_literal: true + +module Ci + module Runners + class GetUsageServiceBase + include Gitlab::Utils::StrongMemoize + + def initialize(current_user, runner_type:, from_date:, to_date:, max_item_count:, additional_group_by_columns: []) + @current_user = current_user + @runner_type = Ci::Runner.runner_types[runner_type] + @from_date = from_date + @to_date = to_date + @max_item_count = max_item_count + @additional_group_by_columns = additional_group_by_columns + end + + def execute + unless ::Gitlab::ClickHouse.configured? + return ServiceResponse.error(message: 'ClickHouse database is not configured', + reason: :db_not_configured) + end + + unless Ability.allowed?(@current_user, :read_runner_usage) + return ServiceResponse.error(message: 'Insufficient permissions', + reason: :insufficient_permissions) + end + + data = ClickHouse::Client.select(clickhouse_query, :main) + ServiceResponse.success(payload: data) + end + + private + + attr_reader :runner_type, :from_date, :to_date, :max_item_count, :additional_group_by_columns + + def clickhouse_query + grouping_columns = ["#{bucket_column}_bucket", *additional_group_by_columns].join(', ') + raw_query = <<~SQL.squish + WITH top_buckets AS + ( + SELECT #{bucket_column} AS #{bucket_column}_bucket + FROM #{table_name} + WHERE #{where_conditions} + GROUP BY #{bucket_column} + ORDER BY sumSimpleState(total_duration) DESC + LIMIT {max_item_count: UInt64} + ) + SELECT + IF(#{table_name}.#{bucket_column} IN top_buckets, #{table_name}.#{bucket_column}, NULL) + AS #{bucket_column}_bucket, + #{select_list} + FROM #{table_name} + WHERE #{where_conditions} + GROUP BY #{grouping_columns} + ORDER BY #{order_list} + SQL + + ClickHouse::Client::Query.new(raw_query: raw_query, placeholders: placeholders) + end + + def table_name + raise NotImplementedError + end + + def bucket_column + raise NotImplementedError + end + + def select_list + [ + *additional_group_by_columns, + 'countMerge(count_builds) AS count_builds', + 'toUInt64(sumSimpleState(total_duration) / 60000) AS total_duration_in_mins' + ].join(', ') + end + strong_memoize_attr :select_list + + def order_list + [ + "(#{bucket_column}_bucket IS NULL)", + 'total_duration_in_mins DESC', + "#{bucket_column}_bucket ASC" + ].join(', ') + end + strong_memoize_attr :order_list + + def where_conditions + <<~SQL + #{'runner_type = {runner_type: UInt8} AND' if runner_type} + finished_at_bucket >= {from_date: DateTime('UTC', 6)} AND + finished_at_bucket < {to_date: DateTime('UTC', 6)} + SQL + end + strong_memoize_attr :where_conditions + + def placeholders + { + runner_type: runner_type, + from_date: format_date(from_date), + to_date: format_date(to_date + 1), # Include jobs until the end of the day + max_item_count: max_item_count + }.compact + end + + def format_date(date) + date.strftime('%Y-%m-%d %H:%M:%S') + end + end + end +end diff --git a/ee/spec/services/ci/runners/get_usage_by_project_service_spec.rb b/ee/spec/services/ci/runners/get_usage_by_project_service_spec.rb index 964f516804534b476a862bc6af406ff84f2ee4f0..ba3fe6e15662713ccd0aef1e629b05b5b4ec28bf 100644 --- a/ee/spec/services/ci/runners/get_usage_by_project_service_spec.rb +++ b/ee/spec/services/ci/runners/get_usage_by_project_service_spec.rb @@ -29,11 +29,11 @@ let(:runner_type) { nil } let(:from_date) { Date.new(2023, 12, 1) } let(:to_date) { Date.new(2023, 12, 31) } - let(:max_project_count) { 50 } - let(:group_by_columns) { [] } + let(:max_item_count) { 50 } + let(:additional_group_by_columns) { [] } let(:service) do described_class.new(user, runner_type: runner_type, from_date: from_date, to_date: to_date, - group_by_columns: group_by_columns, max_project_count: max_project_count) + additional_group_by_columns: additional_group_by_columns, max_item_count: max_item_count) end let(:result) { service.execute } @@ -74,51 +74,49 @@ end it 'exports usage data' do - is_expected.to eq( - [{ "grouped_project_id" => builds.last.project.id, "count_builds" => 4, "total_duration_in_mins" => 140 }, - { "grouped_project_id" => builds[3].project.id, "count_builds" => 1, "total_duration_in_mins" => 17 }, - { "grouped_project_id" => builds[2].project.id, "count_builds" => 1, "total_duration_in_mins" => 16 }, - { "grouped_project_id" => builds[1].project.id, "count_builds" => 1, "total_duration_in_mins" => 15 }, - { "grouped_project_id" => builds[0].project.id, "count_builds" => 1, "total_duration_in_mins" => 14 }] - ) + is_expected.to eq([ + { 'project_id_bucket' => builds.last.project.id, 'count_builds' => 4, 'total_duration_in_mins' => 140 }, + { 'project_id_bucket' => builds[3].project.id, 'count_builds' => 1, 'total_duration_in_mins' => 17 }, + { 'project_id_bucket' => builds[2].project.id, 'count_builds' => 1, 'total_duration_in_mins' => 16 }, + { 'project_id_bucket' => builds[1].project.id, 'count_builds' => 1, 'total_duration_in_mins' => 15 }, + { 'project_id_bucket' => builds[0].project.id, 'count_builds' => 1, 'total_duration_in_mins' => 14 } + ]) end - context 'when group_by_columns specified' do - let(:group_by_columns) { [:status, :runner_type] } + context 'when additional_group_by_columns specified' do + let(:additional_group_by_columns) { [:status, :runner_type] } it 'exports usage data grouped by status and runner_type' do - is_expected.to eq( - [ - { "grouped_project_id" => builds.last.project.id, "status" => "failed", "runner_type" => 2, - "count_builds" => 1, "total_duration_in_mins" => 120 }, - { "grouped_project_id" => builds[3].project.id, "status" => "skipped", "runner_type" => 1, - "count_builds" => 1, "total_duration_in_mins" => 17 }, - { "grouped_project_id" => builds[2].project.id, "status" => "canceled", "runner_type" => 1, - "count_builds" => 1, "total_duration_in_mins" => 16 }, - { "grouped_project_id" => builds[1].project.id, "status" => "failed", "runner_type" => 1, "count_builds" => 1, - "total_duration_in_mins" => 15 }, - { "grouped_project_id" => builds[0].project.id, "status" => "success", "runner_type" => 1, - "count_builds" => 1, "total_duration_in_mins" => 14 }, - { "grouped_project_id" => builds.last.project.id, "status" => "failed", "runner_type" => 1, - "count_builds" => 1, "total_duration_in_mins" => 10 }, - { "grouped_project_id" => builds.last.project.id, "status" => "success", "runner_type" => 1, - "count_builds" => 1, "total_duration_in_mins" => 7 }, - { "grouped_project_id" => builds.last.project.id, "status" => "canceled", "runner_type" => 2, - "count_builds" => 1, "total_duration_in_mins" => 3 } - ] - ) + is_expected.to eq([ + { 'project_id_bucket' => builds.last.project.id, 'status' => 'failed', 'runner_type' => 2, + 'count_builds' => 1, 'total_duration_in_mins' => 120 }, + { 'project_id_bucket' => builds[3].project.id, 'status' => 'skipped', 'runner_type' => 1, + 'count_builds' => 1, 'total_duration_in_mins' => 17 }, + { 'project_id_bucket' => builds[2].project.id, 'status' => 'canceled', 'runner_type' => 1, + 'count_builds' => 1, 'total_duration_in_mins' => 16 }, + { 'project_id_bucket' => builds[1].project.id, 'status' => 'failed', 'runner_type' => 1, 'count_builds' => 1, + 'total_duration_in_mins' => 15 }, + { 'project_id_bucket' => builds[0].project.id, 'status' => 'success', 'runner_type' => 1, + 'count_builds' => 1, 'total_duration_in_mins' => 14 }, + { 'project_id_bucket' => builds.last.project.id, 'status' => 'failed', 'runner_type' => 1, + 'count_builds' => 1, 'total_duration_in_mins' => 10 }, + { 'project_id_bucket' => builds.last.project.id, 'status' => 'success', 'runner_type' => 1, + 'count_builds' => 1, 'total_duration_in_mins' => 7 }, + { 'project_id_bucket' => builds.last.project.id, 'status' => 'canceled', 'runner_type' => 2, + 'count_builds' => 1, 'total_duration_in_mins' => 3 } + ]) end end - context "when max_project_count doesn't fit all projects" do - let(:max_project_count) { 2 } + context 'when the number of projects exceeds max_item_count' do + let(:max_item_count) { 2 } it 'exports usage data for the 2 top projects plus aggregate for other projects' do - is_expected.to eq( - [{ "grouped_project_id" => builds.last.project.id, "count_builds" => 4, "total_duration_in_mins" => 140 }, - { "grouped_project_id" => builds[3].project.id, "count_builds" => 1, "total_duration_in_mins" => 17 }, - { "grouped_project_id" => nil, "count_builds" => 3, "total_duration_in_mins" => 45 }] - ) + is_expected.to eq([ + { 'project_id_bucket' => builds.last.project.id, 'count_builds' => 4, 'total_duration_in_mins' => 140 }, + { 'project_id_bucket' => builds[3].project.id, 'count_builds' => 1, 'total_duration_in_mins' => 17 }, + { 'project_id_bucket' => nil, 'count_builds' => 3, 'total_duration_in_mins' => 45 } + ]) end end @@ -126,8 +124,8 @@ let(:runner_type) { :group_type } it 'exports usage data for runners of specified type' do - is_expected.to eq( - [{ "grouped_project_id" => builds.last.project.id, "count_builds" => 2, "total_duration_in_mins" => 123 }] + is_expected.to contain_exactly( + { 'project_id_bucket' => builds.last.project.id, 'count_builds' => 2, 'total_duration_in_mins' => 123 } ) end end @@ -154,7 +152,9 @@ let(:builds) { [build_before, build_in_range, build_overflowing_the_range, build_after] } it 'only exports usage data for builds created in the date range' do - is_expected.to eq([{ "grouped_project_id" => project.id, "count_builds" => 2, "total_duration_in_mins" => 172 }]) + is_expected.to contain_exactly( + { 'project_id_bucket' => project.id, 'count_builds' => 2, 'total_duration_in_mins' => 172 } + ) end end diff --git a/ee/spec/services/ci/runners/get_usage_service_spec.rb b/ee/spec/services/ci/runners/get_usage_service_spec.rb index 81baac61e5a728172e713884d71f70ea845d1ad6..c02e0d9894046feb0313080b9e424ea6b749bd29 100644 --- a/ee/spec/services/ci/runners/get_usage_service_spec.rb +++ b/ee/spec/services/ci/runners/get_usage_service_spec.rb @@ -2,8 +2,7 @@ require 'spec_helper' -RSpec.describe Ci::Runners::GetUsageService, :click_house, :enable_admin_mode, - feature_category: :fleet_visibility do +RSpec.describe Ci::Runners::GetUsageService, :click_house, :enable_admin_mode, feature_category: :fleet_visibility do let_it_be(:project) { create(:project) } let_it_be(:instance_runners) { create_list(:ci_runner, 3, :instance, :with_runner_manager) } let_it_be(:group) { create(:group) } @@ -25,10 +24,10 @@ let(:runner_type) { nil } let(:from_date) { Date.new(2023, 12, 1) } let(:to_date) { Date.new(2023, 12, 31) } - let(:max_runners_count) { 50 } + let(:max_item_count) { 50 } let(:service) do described_class.new(user, runner_type: runner_type, from_date: from_date, to_date: to_date, - max_runners_count: max_runners_count) + max_item_count: max_item_count) end let(:result) { service.execute } @@ -65,22 +64,22 @@ it 'exports usage data by runner' do is_expected.to eq( - [{ "runner_id" => group_runner.id, "count_builds" => 5, "total_duration_in_mins" => 500 }] + - instance_runners.each_with_index.map do |runner, index| - { "runner_id" => runner.id, "count_builds" => index + 1, "total_duration_in_mins" => 10 * (index + 1) } - end.reverse + [{ 'runner_id_bucket' => group_runner.id, 'count_builds' => 5, 'total_duration_in_mins' => 500 }] + + instance_runners.each_with_index.map do |runner, index| + { 'runner_id_bucket' => runner.id, 'count_builds' => index + 1, 'total_duration_in_mins' => 10 * (index + 1) } + end.reverse ) end - context "when max_runners_count doesn't fit all projects" do - let(:max_runners_count) { 2 } + context 'when the number of runners exceeds max_item_count' do + let(:max_item_count) { 2 } - it 'exports usage data for the 2 top projects plus aggregate for other projects' do - is_expected.to eq( - [{ "runner_id" => group_runner.id, "count_builds" => 5, "total_duration_in_mins" => 500 }, - { "runner_id" => instance_runners.last.id, "count_builds" => 3, "total_duration_in_mins" => 30 }, - { "runner_id" => nil, "count_builds" => 3, "total_duration_in_mins" => 30 }] - ) + it 'exports usage data for the 2 top runners plus aggregate for other projects' do + is_expected.to eq([ + { 'runner_id_bucket' => group_runner.id, 'count_builds' => 5, 'total_duration_in_mins' => 500 }, + { 'runner_id_bucket' => instance_runners.last.id, 'count_builds' => 3, 'total_duration_in_mins' => 30 }, + { 'runner_id_bucket' => nil, 'count_builds' => 3, 'total_duration_in_mins' => 30 } + ]) end end @@ -89,7 +88,7 @@ it 'exports usage data for runners of specified type' do is_expected.to eq( - [{ "runner_id" => group_runner.id, "count_builds" => 5, "total_duration_in_mins" => 500 }] + [{ 'runner_id_bucket' => group_runner.id, 'count_builds' => 5, 'total_duration_in_mins' => 500 }] ) end end @@ -116,7 +115,9 @@ let(:builds) { [build_before, build_in_range, build_overflowing_the_range, build_after] } it 'only exports usage data for builds created in the date range' do - is_expected.to eq([{ "runner_id" => group_runner.id, "count_builds" => 2, "total_duration_in_mins" => 172 }]) + is_expected.to contain_exactly( + { 'runner_id_bucket' => group_runner.id, 'count_builds' => 2, 'total_duration_in_mins' => 172 } + ) end end diff --git a/gems/click_house-client/lib/click_house/client/query.rb b/gems/click_house-client/lib/click_house/client/query.rb index 41435d239cfe64d4c8dfca950b4ab4d82503a9ae..65582031a4cd84d40ec87889710cbd4f01857f7a 100644 --- a/gems/click_house-client/lib/click_house/client/query.rb +++ b/gems/click_house-client/lib/click_house/client/query.rb @@ -3,9 +3,9 @@ module ClickHouse module Client class Query < QueryLike - SUBQUERY_PLACEHOLDER_REGEX = /{\w+:Subquery}/ # exmaple: {var:Subquery}, special "internal" type for subqueries - PLACEHOLDER_REGEX = /{\w+:\w+}/ # exmaple: {var:UInt8} - PLACEHOLDER_NAME_REGEX = /{(\w+):/ # exmaple: {var:UInt8} => var + SUBQUERY_PLACEHOLDER_REGEX = /{\w+:Subquery}/ # example: {var:Subquery}, special "internal" type for subqueries + PLACEHOLDER_REGEX = /{\w+:\w+}/ # example: {var:UInt8} + PLACEHOLDER_NAME_REGEX = /{(\w+):/ # example: {var:UInt8} => var def initialize(raw_query:, placeholders: {}) raise QueryError, 'Empty query string given' if raw_query.blank?