Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add maintenance window logic #115

Merged
merged 3 commits into from
Feb 29, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions config/locales/en.yml
Original file line number Diff line number Diff line change
Expand Up @@ -67,3 +67,9 @@ en:
return a boolean indicating whether the page is cached and will
be served by rack middleware.
default: proc { |_rack_env| false }
bigquery_maintenance_window:
description: |
Schedule a maintenance window during which no events are streamed to BigQuery
in the format of '22-01-2024 19:30..22-01-2024 20:30' (UTC).
default: ENV['BIGQUERY_MAINTENANCE_WINDOW']

39 changes: 39 additions & 0 deletions lib/dfe/analytics.rb
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ def self.config
pseudonymise_web_request_user_id
entity_table_checks_enabled
rack_page_cached
bigquery_maintenance_window
]

@config ||= Struct.new(*configurables).new
Expand All @@ -86,6 +87,7 @@ def self.configure
config.pseudonymise_web_request_user_id ||= false
config.entity_table_checks_enabled ||= false
config.rack_page_cached ||= proc { |_rack_env| false }
config.bigquery_maintenance_window ||= ENV.fetch('BIGQUERY_MAINTENANCE_WINDOW', nil)
end

def self.initialize!
Expand Down Expand Up @@ -244,5 +246,42 @@ def self.rack_page_cached?(rack_env)
def self.entity_table_checks_enabled?
config.entity_table_checks_enabled
end

def self.parse_maintenance_window
return [nil, nil] unless config.bigquery_maintenance_window

start_str, end_str = config.bigquery_maintenance_window.split('..', 2).map(&:strip)
begin
parsed_start_time = DateTime.strptime(start_str, '%d-%m-%Y %H:%M')
parsed_end_time = DateTime.strptime(end_str, '%d-%m-%Y %H:%M')

start_time = Time.zone.parse(parsed_start_time.to_s)
end_time = Time.zone.parse(parsed_end_time.to_s)

if start_time > end_time
Rails.logger.info('Start time is after end time in maintenance window configuration')
return [nil, nil]
end

ericaporter marked this conversation as resolved.
Show resolved Hide resolved
[start_time, end_time]
rescue ArgumentError => e
Rails.logger.info("DfE::Analytics: Unexpected error in maintenance window configuration: #{e.message}")
[nil, nil]
end
end

def self.within_maintenance_window?
start_time, end_time = parse_maintenance_window
return false unless start_time && end_time

Time.zone.now.between?(start_time, end_time)
end

def self.next_scheduled_time_after_maintenance_window
start_time, end_time = parse_maintenance_window
return unless start_time && end_time

end_time + (Time.zone.now - start_time).seconds
end
ericaporter marked this conversation as resolved.
Show resolved Hide resolved
end
end
4 changes: 3 additions & 1 deletion lib/dfe/analytics/send_events.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,9 @@ def self.do(events)

events = events.map { |event| event.is_a?(Event) ? event.as_json : event }

if DfE::Analytics.async?
if DfE::Analytics.within_maintenance_window?
set(wait_until: DfE::Analytics.next_scheduled_time_after_maintenance_window).perform_later(events)
elsif DfE::Analytics.async?
perform_later(events)
else
perform_now(events)
Expand Down
55 changes: 55 additions & 0 deletions spec/dfe/analytics/send_events_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -162,5 +162,60 @@
end
end
end

describe 'maintenance window scheduling' do
let(:events) { [event] }
let(:maintenance_window_start) { Time.zone.parse('25-02-2024 08:00') }
let(:maintenance_window_end) { Time.zone.parse('25-02-2024 10:00') }
let(:current_time_within_window) { Time.zone.parse('25-02-2024 09:00') }

before do
allow(DfE::Analytics).to receive(:within_maintenance_window?).and_return(true)
allow(DfE::Analytics.config).to receive(:bigquery_maintenance_window).and_return('25-02-2024 08:00..25-02-2024 10:00')
Timecop.freeze(current_time_within_window)
end

after do
Timecop.return
end

context 'within the maintenance window' do
it 'does not enqueue the events for asynchronous execution' do
expect(DfE::Analytics::SendEvents).not_to receive(:perform_later).with(events)
DfE::Analytics::SendEvents.do(events)
end

it 'does not execute the events synchronously' do
expect(DfE::Analytics::SendEvents).not_to receive(:perform_now).with(events)
DfE::Analytics::SendEvents.do(events)
end

it 'schedules the events for after the maintenance window' do
elapsed_seconds = current_time_within_window - maintenance_window_start
expected_wait_until = maintenance_window_end + elapsed_seconds

expect(DfE::Analytics::SendEvents).to receive(:set).with(wait_until: expected_wait_until).and_call_original
DfE::Analytics::SendEvents.do(events)
end
end
ericaporter marked this conversation as resolved.
Show resolved Hide resolved

context 'outside the mainenance window' do
before do
allow(DfE::Analytics).to receive(:within_maintenance_window?).and_return(false)
end

it 'enqueues the events for asynchronous execution' do
allow(DfE::Analytics).to receive(:async?).and_return(true)
expect(DfE::Analytics::SendEvents).to receive(:perform_later).with(events)
DfE::Analytics::SendEvents.do(events)
end

it 'executes the events synchronously' do
allow(DfE::Analytics).to receive(:async?).and_return(false)
expect(DfE::Analytics::SendEvents).to receive(:perform_now).with(events)
DfE::Analytics::SendEvents.do(events)
end
end
end
end
end
63 changes: 63 additions & 0 deletions spec/dfe/analytics_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -184,4 +184,67 @@
end
end
end

describe '.parse_maintenance_window' do
context 'with a valid maintenance window' do
before do
allow(described_class.config).to receive(:bigquery_maintenance_window)
.and_return('01-01-2020 00:00..01-01-2020 23:59')
end

it 'returns the correct start and end times' do
start_time, end_time = described_class.parse_maintenance_window
expect(start_time).to eq(DateTime.new(2020, 1, 1, 0, 0))
expect(end_time).to eq(DateTime.new(2020, 1, 1, 23, 59))
end
end

context 'when start time is after end time' do
before do
allow(described_class.config).to receive(:bigquery_maintenance_window)
.and_return('01-01-2020 23:59..01-01-2020 00:00')
end

it 'logs an error and returns [nil, nil]' do
expect(Rails.logger).to receive(:info).with(/Start time is after end time/)
expect(described_class.parse_maintenance_window).to eq([nil, nil])
end
end

context 'with an invalid format' do
before do
allow(described_class.config).to receive(:bigquery_maintenance_window)
.and_return('invalid_format')
end

it 'logs an error and returns [nil, nil]' do
expect(Rails.logger).to receive(:info).with(/Unexpected error/)
expect(described_class.parse_maintenance_window).to eq([nil, nil])
end
end
end

describe '.within_maintenance_window?' do
context 'when the current time is within the maintenance window' do
before do
allow(described_class).to receive(:parse_maintenance_window)
.and_return([DateTime.now - 1.hour, DateTime.now + 1.hour])
end

it 'returns true' do
expect(described_class.within_maintenance_window?).to be true
end
end

context 'when the current time is outside the maintenance window' do
before do
allow(described_class).to receive(:parse_maintenance_window)
.and_return([DateTime.now - 2.days, DateTime.now - 1.day])
end

it 'returns false' do
expect(described_class.within_maintenance_window?).to be false
end
end
end
end
Loading