diff --git a/app/services/ci/collect_pipeline_analytics_service.rb b/app/services/ci/collect_pipeline_analytics_service.rb index 3508df9df685b636b4f798a4e66b32c11189153c..bb9d4d2a9abc053884e25cf49b189f5bdc8700b3 100644 --- a/app/services/ci/collect_pipeline_analytics_service.rb +++ b/app/services/ci/collect_pipeline_analytics_service.rb @@ -2,8 +2,6 @@ module Ci class CollectPipelineAnalyticsService - TIME_BUCKETS_LIMIT = 1.week.in_hours + 1 # +1 to add some error margin - STATUS_GROUP_TO_STATUSES = { success: %w[success], failed: %w[failed], other: %w[canceled skipped] }.freeze STATUS_GROUPS = STATUS_GROUP_TO_STATUSES.keys.freeze STATUS_TO_STATUS_GROUP = STATUS_GROUP_TO_STATUSES.flat_map { |k, v| v.product([k]) }.to_h @@ -25,9 +23,8 @@ def execute return ServiceResponse.error(message: 'Not allowed') unless allowed? - if (@to_time - @from_time) / 1.hour > TIME_BUCKETS_LIMIT - return ServiceResponse.error(message: "Maximum of #{TIME_BUCKETS_LIMIT} 1-hour intervals can be requested") - end + error_message = clickhouse_model.validate_time_window(@from_time, @to_time) + return ServiceResponse.error(message: error_message) if error_message ServiceResponse.success(payload: { aggregate: calculate_aggregate }) end @@ -39,7 +36,11 @@ def allowed? end def clickhouse_model - ::ClickHouse::Models::Ci::FinishedPipelinesHourly + if ::ClickHouse::Models::Ci::FinishedPipelinesHourly.time_window_valid?(@from_time, @to_time) + return ::ClickHouse::Models::Ci::FinishedPipelinesHourly + end + + ::ClickHouse::Models::Ci::FinishedPipelinesDaily end def calculate_aggregate diff --git a/lib/click_house/models/ci/finished_pipelines_base.rb b/lib/click_house/models/ci/finished_pipelines_base.rb index e457851391c8423b5d483c6fff7e517f1d5242b9..8b1c7930ae5ebfa66e5900f7fde820f53ab36d59 100644 --- a/lib/click_house/models/ci/finished_pipelines_base.rb +++ b/lib/click_house/models/ci/finished_pipelines_base.rb @@ -4,6 +4,14 @@ module ClickHouse # rubocop:disable Gitlab/BoundedContexts -- Existing module module Models module Ci class FinishedPipelinesBase < ClickHouse::Models::BaseModel + def self.time_window_valid?(from_time, to_time) + raise NotImplementedError, "subclasses of #{self.class.name} must implement #{__method__}" + end + + def self.validate_time_window(from_time, to_time) + raise NotImplementedError, "subclasses of #{self.class.name} must implement #{__method__}" + end + def self.for_project(project) new.for_project(project) end diff --git a/lib/click_house/models/ci/finished_pipelines_daily.rb b/lib/click_house/models/ci/finished_pipelines_daily.rb index 0d761be1a365c4d819b00fcf22d908dc8de421e3..1d903e245f19cc50921fe588572786a2821c65dd 100644 --- a/lib/click_house/models/ci/finished_pipelines_daily.rb +++ b/lib/click_house/models/ci/finished_pipelines_daily.rb @@ -4,9 +4,21 @@ module ClickHouse # rubocop:disable Gitlab/BoundedContexts -- Existing module module Models module Ci class FinishedPipelinesDaily < FinishedPipelinesBase + TIME_BUCKETS_LIMIT = 366 + def self.table_name 'ci_finished_pipelines_daily' end + + def self.time_window_valid?(from_time, to_time) + (to_time - from_time) / 1.day < TIME_BUCKETS_LIMIT + end + + def self.validate_time_window(from_time, to_time) + return if time_window_valid?(from_time, to_time) + + "Maximum of #{TIME_BUCKETS_LIMIT} days can be requested" + end end end end diff --git a/lib/click_house/models/ci/finished_pipelines_hourly.rb b/lib/click_house/models/ci/finished_pipelines_hourly.rb index 16686a6d689eea95ffc6f3a4118c75128bd54c79..88dccbe1e3d4d2007947636488af9f237669749e 100644 --- a/lib/click_house/models/ci/finished_pipelines_hourly.rb +++ b/lib/click_house/models/ci/finished_pipelines_hourly.rb @@ -4,9 +4,21 @@ module ClickHouse # rubocop:disable Gitlab/BoundedContexts -- Existing module module Models module Ci class FinishedPipelinesHourly < FinishedPipelinesBase + TIME_BUCKETS_LIMIT = 1.week.in_hours.to_i + 1 # +1 to add some error margin + def self.table_name 'ci_finished_pipelines_hourly' end + + def self.time_window_valid?(from_time, to_time) + (to_time - from_time) / 1.hour < TIME_BUCKETS_LIMIT + end + + def self.validate_time_window(from_time, to_time) + return if time_window_valid?(from_time, to_time) + + "Maximum of #{TIME_BUCKETS_LIMIT} hours can be requested" + end end end end diff --git a/spec/lib/click_house/models/ci/finished_pipelines_daily_spec.rb b/spec/lib/click_house/models/ci/finished_pipelines_daily_spec.rb index acce86099c16c7d89f4e249b7784b12cca6687f9..ce1bcb234b381bf709eb14362ebc084460d7e5f8 100644 --- a/spec/lib/click_house/models/ci/finished_pipelines_daily_spec.rb +++ b/spec/lib/click_house/models/ci/finished_pipelines_daily_spec.rb @@ -4,4 +4,40 @@ RSpec.describe ClickHouse::Models::Ci::FinishedPipelinesDaily, feature_category: :fleet_visibility do it_behaves_like 'a ci_finished_pipelines aggregation model', :ci_finished_pipelines_daily + + describe '.time_window_valid?', :freeze_time do + subject(:time_window_valid?) { described_class.time_window_valid?(from_time, to_time) } + + context 'with time window of less than 366 days' do + let(:from_time) { 1.second.after(366.days.ago) } + let(:to_time) { Time.current } + + it { is_expected.to eq true } + end + + context 'with time window of 366 days' do + let(:from_time) { 366.days.ago } + let(:to_time) { Time.current } + + it { is_expected.to eq false } + end + end + + describe '.validate_time_window', :freeze_time do + subject(:validate_time_window) { described_class.validate_time_window(from_time, to_time) } + + context 'with time window of less than 366 days' do + let(:from_time) { 1.second.after(366.days.ago) } + let(:to_time) { Time.current } + + it { is_expected.to be_nil } + end + + context 'with time window of 366 days' do + let(:from_time) { 366.days.ago } + let(:to_time) { Time.current } + + it { is_expected.to eq("Maximum of 366 days can be requested") } + end + end end diff --git a/spec/lib/click_house/models/ci/finished_pipelines_hourly_spec.rb b/spec/lib/click_house/models/ci/finished_pipelines_hourly_spec.rb index 2bb09b7e27a135665a983ca329a9858ee826e2c2..cb7b8ba0b5fa9e14648f75e49e11083e7e912e7c 100644 --- a/spec/lib/click_house/models/ci/finished_pipelines_hourly_spec.rb +++ b/spec/lib/click_house/models/ci/finished_pipelines_hourly_spec.rb @@ -4,4 +4,40 @@ RSpec.describe ClickHouse::Models::Ci::FinishedPipelinesHourly, feature_category: :fleet_visibility do it_behaves_like 'a ci_finished_pipelines aggregation model', :ci_finished_pipelines_hourly + + describe '.time_window_valid?', :freeze_time do + subject(:time_window_valid?) { described_class.time_window_valid?(from_time, to_time) } + + context 'with time window of one week and one hour' do + let(:from_time) { (1.hour - 1.second).before(1.week.ago) } + let(:to_time) { Time.current } + + it { is_expected.to eq true } + end + + context 'with time window of one week and 1 hour' do + let(:from_time) { 1.hour.before(1.week.ago) } + let(:to_time) { Time.current } + + it { is_expected.to eq false } + end + end + + describe '.validate_time_window', :freeze_time do + subject(:validate_time_window) { described_class.validate_time_window(from_time, to_time) } + + context 'with time window of one week and one hour' do + let(:from_time) { (1.hour - 1.second).before(1.week.ago) } + let(:to_time) { Time.current } + + it { is_expected.to be_nil } + end + + context 'with time window of one week and 1 hour' do + let(:from_time) { 1.hour.before(1.week.ago) } + let(:to_time) { Time.current } + + it { is_expected.to eq("Maximum of 169 hours can be requested") } + end + end end diff --git a/spec/requests/api/graphql/project/project_pipeline_statistics_spec.rb b/spec/requests/api/graphql/project/project_pipeline_statistics_spec.rb index d08d54e6403dad081cb9c0fd459ae0d5ebbd84c8..987b195b7ec45e78a4a0d91e1f6a79d2f9a02d01 100644 --- a/spec/requests/api/graphql/project/project_pipeline_statistics_spec.rb +++ b/spec/requests/api/graphql/project/project_pipeline_statistics_spec.rb @@ -72,7 +72,7 @@ end end - describe 'aggregate' do + describe 'aggregate', :aggregate_failures do subject(:aggregate) do perform_request @@ -86,6 +86,9 @@ end it "contains expected data for the last week" do + perform_request + + expect_graphql_errors_to_be_empty expect(aggregate).to eq({ 'label' => nil, 'all' => '0', @@ -97,6 +100,9 @@ context 'when there are pipelines in last week', time_travel_to: Time.utc(2024, 5, 11) do it "contains expected data for the last week" do + perform_request + + expect_graphql_errors_to_be_empty expect(aggregate).to eq({ 'label' => nil, 'all' => '4', @@ -112,6 +118,9 @@ let(:to_time) { '2024-05-11T00:00:00+00:00' } it "contains expected data for the period" do + perform_request + + expect_graphql_errors_to_be_empty expect(aggregate).to eq({ 'label' => nil, 'all' => '2', @@ -120,6 +129,35 @@ 'other' => '0' }) end + + context 'with time window spanning less than 1 year' do + let(:from_time) { '2023-05-11T00:00:00+00:00' } + let(:to_time) { '2024-05-10T23:59:59+00:00' } + + it "contains expected data for the period" do + perform_request + + expect_graphql_errors_to_be_empty + expect(aggregate).to eq({ + 'label' => nil, + 'all' => '5', + 'success' => '1', + 'failed' => '2', + 'other' => '1' + }) + end + end + + context 'with time window spanning 1 year' do + let(:from_time) { '2024-01-01T00:00:00+02:00' } + let(:to_time) { '2025-01-01T00:00:00+02:00' } + + it "contains expected data for the period" do + perform_request + + expect_graphql_errors_to_include("Maximum of 366 days can be requested") + end + end end end diff --git a/spec/services/ci/collect_pipeline_analytics_service_spec.rb b/spec/services/ci/collect_pipeline_analytics_service_spec.rb index fbf56774258eeaf2f3ce9352912c8a288d6bcd71..08d2dfbda23874e49fbd468e505cf6f774662cec 100644 --- a/spec/services/ci/collect_pipeline_analytics_service_spec.rb +++ b/spec/services/ci/collect_pipeline_analytics_service_spec.rb @@ -105,6 +105,7 @@ context 'when requesting statistics starting one second before beginning of week' do let(:from_time) { 1.second.before(starting_time) } + let(:to_time) { 1.second.before(ending_time) } it 'does not include job starting 1 second before start of week' do expect(result.errors).to eq([]) @@ -115,6 +116,7 @@ context 'when requesting statistics starting one hour before beginning of week' do let(:from_time) { 1.hour.before(starting_time) } + let(:to_time) { 1.second.before(ending_time) } it 'includes job starting 1 second before start of week' do expect(result.errors).to eq([]) @@ -123,12 +125,11 @@ end end - context 'when requesting hourly statistics that span more than one week' do - let(:from_time) { (1.hour + 1.second).before(starting_time) } + context 'when requesting statistics that span more than one year' do + let(:from_time) { (366.days + 1.second).before(starting_time) } it 'returns an error' do - expect(result.errors).to contain_exactly( - "Maximum of #{described_class::TIME_BUCKETS_LIMIT} 1-hour intervals can be requested") + expect(result.errors).to contain_exactly("Maximum of 366 days can be requested") expect(result.error?).to eq(true) end end