Skip to content

Commit

Permalink
Add maintenance window logic (#115)
Browse files Browse the repository at this point in the history
* Add maintenance window logic
  • Loading branch information
ericaporter authored Feb 29, 2024
1 parent 7b7a313 commit b38abea
Show file tree
Hide file tree
Showing 5 changed files with 166 additions and 1 deletion.
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 @@ -68,6 +68,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 @@ -91,6 +92,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 @@ -249,5 +251,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

[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
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

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

0 comments on commit b38abea

Please sign in to comment.