Skip to content

Commit

Permalink
new: prune the cache when too large
Browse files Browse the repository at this point in the history
  • Loading branch information
ianbayne committed Oct 26, 2024
1 parent 96b8368 commit 4bf848d
Show file tree
Hide file tree
Showing 2 changed files with 138 additions and 5 deletions.
23 changes: 19 additions & 4 deletions lib/valid_email2/address.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,14 @@ module ValidEmail2
class Address
attr_accessor :address

# Cache structure: { domain (String): { records: [], cached_at: Time, ttl: Integer } }
@@mx_servers_cache = {}
@@mx_or_a_servers_cache = {}

PROHIBITED_DOMAIN_CHARACTERS_REGEX = /[+!_\/\s'`]/
DEFAULT_RECIPIENT_DELIMITER = '+'
DOT_DELIMITER = '.'
MAX_CACHE_SIZE = 1_000

# Cache structure: { domain (String): { records: [], cached_at: Time, ttl: Integer } }
@@mx_servers_cache = {}
@@mx_or_a_servers_cache = {}

def self.prohibited_domain_characters_regex
@prohibited_domain_characters_regex ||= PROHIBITED_DOMAIN_CHARACTERS_REGEX
Expand Down Expand Up @@ -141,6 +142,10 @@ def address_contain_emoticons?(email)
end

def mx_servers
if @@mx_servers_cache.size > MAX_CACHE_SIZE
prune_cache(@@mx_servers_cache)
end

domain = address.domain.downcase

if @@mx_servers_cache[domain]
Expand Down Expand Up @@ -170,6 +175,10 @@ def null_mx?
end

def mx_or_a_servers
if @@mx_or_a_servers_cache.size > MAX_CACHE_SIZE
prune_cache(@@mx_or_a_servers_cache)
end

domain = address.domain.downcase

if @@mx_or_a_servers_cache[domain]
Expand All @@ -194,5 +203,11 @@ def mx_or_a_servers

records
end

def prune_cache(cache)
entries_sorted_by_cached_at_asc = (cache.sort_by { |_domain, data| data[:cached_at] }).flatten
entries_to_remove = entries_sorted_by_cached_at_asc.first(cache.size - MAX_CACHE_SIZE)
entries_to_remove.each { |domain| cache.delete(domain) }
end
end
end
120 changes: 119 additions & 1 deletion spec/address_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,65 @@

expect(Resolv::DNS).to have_received(:open).once
end

it "does not prune the cache when the cache size is less than the max cache size" do
expect(email_instance).not_to receive(:prune_cache)

email_instance.valid_strict_mx?
end

it "prunes the cache when the cache size is greater than the max cache size" do
stub_const("#{described_class}::MAX_CACHE_SIZE", 0)

expect(email_instance).to receive(:prune_cache).with(described_class.class_variable_get(:@@mx_servers_cache)).once

email_instance.valid_strict_mx?
email_instance.valid_strict_mx?
end

it "does not call the MX or A servers lookup when there is a cached entry for the domain and the cache size is less than the max cache size" do
stub_const("#{described_class}::MAX_CACHE_SIZE", 1)
described_class.class_variable_set(:@@mx_servers_cache, { email_instance.address.domain => { records: mock_mx_records, cached_at: Time.now, ttl: ttl }})

email_instance.valid_strict_mx?

expect(Resolv::DNS).not_to have_received(:open)
end

it "calls the MX or A servers lookup when there is a cached entry for the domain but the cache size is greater than the max cache size" do
stub_const("#{described_class}::MAX_CACHE_SIZE", 0)
described_class.class_variable_set(:@@mx_servers_cache, { email_instance.address.domain => { records: mock_mx_records, cached_at: Time.now, ttl: ttl }})

email_instance.valid_strict_mx?

expect(Resolv::DNS).to have_received(:open).once
end

it "does not prune older entries when the cache size is less than the max size" do
stub_const("#{described_class}::MAX_CACHE_SIZE", 1)
described_class.class_variable_set(:@@mx_servers_cache, {
'another_domain.com' => {
records: mock_mx_records, cached_at: Time.now - 100, ttl: ttl
}
})

email_instance.valid_strict_mx?

expect(described_class.class_variable_get(:@@mx_servers_cache).keys).to match_array([email_instance.address.domain, 'another_domain.com'])
end

it "prunes older entries when the cache size is greater than the max size" do
stub_const("#{described_class}::MAX_CACHE_SIZE", 0)
described_class.class_variable_set(:@@mx_servers_cache, {
'another_domain.com' => {
records: mock_mx_records, cached_at: Time.now - 100, ttl: ttl
}
})

email_instance.valid_strict_mx?

expect(described_class.class_variable_get(:@@mx_servers_cache).keys).to match_array([email_instance.address.domain])
end
end

describe "#valid_mx?" do
Expand Down Expand Up @@ -148,11 +207,70 @@

it "calls the MX or A servers lookup when the time since last lookup is greater than the cached ttl entry" do
described_class.class_variable_set(:@@mx_or_a_servers_cache, { email_instance.address.domain => { records: mock_a_records, cached_at: Time.now - ttl, ttl: ttl }})

email_instance.valid_mx?

expect(Resolv::DNS).to have_received(:open).once
end

it "does not prune the cache when the cache size is less than the max cache size" do
expect(email_instance).not_to receive(:prune_cache)

email_instance.valid_mx?
end

it "prunes the cache when the cache size is greater than the max cache size" do
stub_const("#{described_class}::MAX_CACHE_SIZE", 0)

expect(email_instance).to receive(:prune_cache).with(described_class.class_variable_get(:@@mx_or_a_servers_cache)).once

email_instance.valid_mx?
email_instance.valid_mx?
end

it "does not call the MX or A servers lookup when there is a cached entry for the domain and the cache size is less than the max cache size" do
stub_const("#{described_class}::MAX_CACHE_SIZE", 1)
described_class.class_variable_set(:@@mx_or_a_servers_cache, { email_instance.address.domain => { records: mock_a_records, cached_at: Time.now, ttl: ttl }})

email_instance.valid_mx?

expect(Resolv::DNS).not_to have_received(:open)
end

it "calls the MX or A servers lookup when there is a cached entry for the domain but the cache size is greater than the max cache size" do
stub_const("#{described_class}::MAX_CACHE_SIZE", 0)
described_class.class_variable_set(:@@mx_or_a_servers_cache, { email_instance.address.domain => { records: mock_a_records, cached_at: Time.now, ttl: ttl }})

email_instance.valid_mx?

expect(Resolv::DNS).to have_received(:open).once
end

it "does not prune older entries when the cache size is less than the max size" do
stub_const("#{described_class}::MAX_CACHE_SIZE", 1)
described_class.class_variable_set(:@@mx_or_a_servers_cache, {
'another_domain.com' => {
records: mock_a_records, cached_at: Time.now - 100, ttl: ttl
}
})

email_instance.valid_mx?

expect(described_class.class_variable_get(:@@mx_or_a_servers_cache).keys).to match_array([email_instance.address.domain, 'another_domain.com'])
end

it "prunes older entries when the cache size is greater than the max size" do
stub_const("#{described_class}::MAX_CACHE_SIZE", 0)
described_class.class_variable_set(:@@mx_or_a_servers_cache, {
'another_domain.com' => {
records: mock_a_records, cached_at: Time.now - 100, ttl: ttl
}
})

email_instance.valid_mx?

expect(described_class.class_variable_get(:@@mx_or_a_servers_cache).keys).to match_array([email_instance.address.domain])
end
end
end
end

0 comments on commit 4bf848d

Please sign in to comment.