Skip to content

Commit 045ed76

Browse files
committed
Implement quota for auto evaluation
This adds the ability to limit the number of autoevaluations that can be run per hour. If the limit is reached, further evaluations are skipped and a warning is logged. We've set the limit to 300 evaluations per hour based on discussion with data science. We're treating topic tagging as an auto evaluation. In order to reuse the quota functionality i've updated the AnswerTopicsJob to ingerit from the base job and added the quota checks.
1 parent f66f79e commit 045ed76

7 files changed

Lines changed: 58 additions & 2 deletions

File tree

app/jobs/answer_analysis/answer_relevancy_job.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ module AnswerAnalysis
22
class AnswerRelevancyJob < BaseJob
33
def perform(answer_id)
44
return unless eligible_for_answer_analysis?(answer_id)
5+
return if quota_limit_reached?
56

67
answer = Answer.includes(:question, :answer_relevancy_aggregate).find(answer_id)
78
return logger.warn(aggregate_exists_warn_message(answer.id)) if answer.answer_relevancy_aggregate.present?

app/jobs/answer_analysis/base_job.rb

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,5 +15,18 @@ def eligible_for_answer_analysis?(answer_id)
1515

1616
eligible
1717
end
18+
19+
def quota_limit_reached?
20+
current_count = Rails.cache.read("auto_evaluations_count")
21+
quota = Rails.configuration.max_auto_evaluations_per_hour
22+
23+
if current_count.present? && current_count >= quota
24+
logger.warn("Auto-evaluation quota limit of #{quota} evaluations per hour reached")
25+
return true
26+
end
27+
28+
Rails.cache.increment("auto_evaluations_count", expires_at: Time.current.at_end_of_hour)
29+
false
30+
end
1831
end
1932
end

app/jobs/answer_analysis/tag_topics_job.rb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
module AnswerAnalysis
2-
class TagTopicsJob < ApplicationJob
3-
MAX_RETRIES = 5
2+
class TagTopicsJob < BaseJob
43
retry_on Anthropic::Errors::APIError, wait: 1.minute, attempts: MAX_RETRIES
54

65
def perform(answer_id)
@@ -11,6 +10,7 @@ def perform(answer_id)
1110
unless answer.eligible_for_topic_analysis?
1211
return logger.info("Answer #{answer_id} is not eligible for topic analysis")
1312
end
13+
return if quota_limit_reached?
1414

1515
result = AutoEvaluation::TopicTagger.call(answer.question_used)
1616

config/application.rb

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,5 +91,7 @@ class Application < Rails::Application
9191
.topic_tagger
9292
.dig("tool_spec", "input_schema", "$defs", "govuk_topic_tags", "enum")
9393
.sort
94+
95+
config.max_auto_evaluations_per_hour = 300
9496
end
9597
end

spec/jobs/answer_analysis/answer_relevancy_job_spec.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
end
1818

1919
it_behaves_like "a job in queue", "default"
20+
it_behaves_like "a job that adheres to the auto_evaluation quota", AutoEvaluation::AnswerRelevancy
2021

2122
describe "#perform" do
2223
it "calls AutoEvaluation::AnswerRelevancy the configured number of times with the correct arguments" do

spec/jobs/answer_analysis/tag_topics_job_spec.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
before { allow(AutoEvaluation::TopicTagger).to receive(:call).and_return(topic_tagger_result) }
2020

2121
it_behaves_like "a job in queue", "default"
22+
it_behaves_like "a job that adheres to the auto_evaluation quota", AutoEvaluation::TopicTagger
2223

2324
describe "#perform" do
2425
it "calls the AutoEvaluation::TopicTagger with the answer message" do

spec/support/job_examples.rb

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,42 @@ module JobExamples
44
expect(described_class.queue_name).to eq(expected_queue)
55
end
66
end
7+
8+
shared_examples "a job that adheres to the auto_evaluation quota" do |metric|
9+
let(:answer) { create(:answer) }
10+
11+
before do
12+
memory_store = ActiveSupport::Cache::MemoryStore.new
13+
allow(Rails).to receive(:cache).and_return(memory_store)
14+
end
15+
16+
it "writes the auto_evaluations_count cache key on the first evaluation" do
17+
described_class.new.perform(answer.id)
18+
expect(Rails.cache.read("auto_evaluations_count")).to eq(1)
19+
end
20+
21+
it "increments the auto_evaluations_count cache key for subsequent evaluations" do
22+
Rails.cache.write("auto_evaluations_count", 1)
23+
described_class.new.perform(answer.id)
24+
expect(Rails.cache.read("auto_evaluations_count")).to eq(2)
25+
end
26+
27+
it "logs a warning and does not perform the evaluation when quota limit is reached" do
28+
max_evaluations = Rails.configuration.max_auto_evaluations_per_hour
29+
Rails.cache.write("auto_evaluations_count", max_evaluations)
30+
expect(described_class.logger)
31+
.to receive(:warn)
32+
.with("Auto-evaluation quota limit of #{max_evaluations} evaluations per hour reached")
33+
expect(metric).not_to receive(:call)
34+
35+
described_class.new.perform(answer.id)
36+
end
37+
38+
it "expires the auto_evaluations_count cache key at the end of the hour" do
39+
described_class.new.perform(answer.id)
40+
travel_to(Time.current.at_end_of_hour + 1.minute) do
41+
expect(Rails.cache.read("auto_evaluations_count")).to be_nil
42+
end
43+
end
44+
end
745
end

0 commit comments

Comments
 (0)