Skip to content

Commit

Permalink
change: move caching logic to separate class
Browse files Browse the repository at this point in the history
  • Loading branch information
ianbayne committed Nov 2, 2024
1 parent 4bf848d commit aaa346f
Show file tree
Hide file tree
Showing 5 changed files with 231 additions and 200 deletions.
66 changes: 12 additions & 54 deletions lib/valid_email2/address.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
require "valid_email2"
require "resolv"
require "mail"
require "valid_email2/dns_records_cache"

module ValidEmail2
class Address
Expand All @@ -11,7 +12,6 @@ class Address
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 = {}
Expand Down Expand Up @@ -142,72 +142,30 @@ 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
@mx_servers_cache ||= ValidEmail2::DnsRecordsCache.new

if @@mx_servers_cache[domain]
cache_entry = @@mx_servers_cache[domain]
if (Time.now - cache_entry[:cached_at]) < cache_entry[:ttl]
return cache_entry[:records]
else
@@mx_servers_cache.delete(domain)
@mx_servers_cache.fetch(address.domain.downcase) do
Resolv::DNS.open(@resolv_config) do |dns|
dns.timeouts = @dns_timeout
dns.getresources(address.domain, Resolv::DNS::Resource::IN::MX)
end
end

records = Resolv::DNS.open(@resolv_config) do |dns|
dns.timeouts = @dns_timeout
dns.getresources(address.domain, Resolv::DNS::Resource::IN::MX)
end

if records.any?
ttl = records.map(&:ttl).min
@@mx_servers_cache[domain] = { records: records, cached_at: Time.now, ttl: ttl }
end

records
end

def null_mx?
mx_servers.length == 1 && mx_servers.first.preference == 0 && mx_servers.first.exchange.length == 0
end

def mx_or_a_servers
if @@mx_or_a_servers_cache.size > MAX_CACHE_SIZE
prune_cache(@@mx_or_a_servers_cache)
end
@mx_or_a_servers_cache ||= ValidEmail2::DnsRecordsCache.new

domain = address.domain.downcase

if @@mx_or_a_servers_cache[domain]
cache_entry = @@mx_or_a_servers_cache[domain]
if (Time.now - cache_entry[:cached_at]) < cache_entry[:ttl]
return cache_entry[:records]
else
@@mx_or_a_servers_cache.delete(domain)
@mx_or_a_servers_cache.fetch(address.domain.downcase) do
Resolv::DNS.open(@resolv_config) do |dns|
dns.timeouts = @dns_timeout
(mx_servers.any? && mx_servers) ||
dns.getresources(address.domain, Resolv::DNS::Resource::IN::A)
end
end

records = Resolv::DNS.open(@resolv_config) do |dns|
dns.timeouts = @dns_timeout
(mx_servers.any? && mx_servers) ||
dns.getresources(address.domain, Resolv::DNS::Resource::IN::A)
end

if records.any?
ttl = records.map(&:ttl).min
@@mx_or_a_servers_cache[domain] = { records: records, cached_at: Time.now, ttl: ttl }
end

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
37 changes: 37 additions & 0 deletions lib/valid_email2/dns_records_cache.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
module ValidEmail2
class DnsRecordsCache
MAX_CACHE_SIZE = 1_000

def initialize
# Cache structure: { domain (String): { records: [], cached_at: Time, ttl: Integer } }
@cache = {}
end

def fetch(domain, &block)
prune(@cache) if @cache.size > MAX_CACHE_SIZE

cache_entry = @cache[domain]

if cache_entry && (Time.now - cache_entry[:cached_at]) < cache_entry[:ttl]
return cache_entry[:records]
else
@cache.delete(domain)
end

records = block.call

if records.any?
ttl = records.map(&:ttl).min
@cache[domain] = { records: records, cached_at: Time.now, ttl: ttl }
end

records
end

def prune(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
Loading

0 comments on commit aaa346f

Please sign in to comment.