2020-07-10 05:05:55 -04:00
|
|
|
# frozen_string_literal: true
|
|
|
|
|
|
|
|
require "demon/base"
|
|
|
|
|
|
|
|
class Demon::EmailSync < ::Demon::Base
|
2024-10-15 22:09:07 -04:00
|
|
|
HEARTBEAT_KEY = "email_sync_heartbeat"
|
|
|
|
HEARTBEAT_INTERVAL = 60.seconds
|
2020-07-10 05:05:55 -04:00
|
|
|
|
|
|
|
def self.prefix
|
|
|
|
"email_sync"
|
|
|
|
end
|
|
|
|
|
2024-12-18 21:10:11 -05:00
|
|
|
def self.max_email_sync_rss
|
|
|
|
return 0 if demons.empty?
|
|
|
|
|
|
|
|
email_sync_pids = demons.map { |uid, demon| demon.pid }
|
|
|
|
|
|
|
|
return 0 if email_sync_pids.empty?
|
|
|
|
|
|
|
|
rss =
|
|
|
|
`ps -eo pid,rss,args | grep '#{email_sync_pids.join("|")}' | grep -v grep | awk '{print $2}'`.split(
|
|
|
|
"\n",
|
|
|
|
)
|
|
|
|
.map(&:to_i)
|
|
|
|
.max
|
|
|
|
|
|
|
|
(rss || 0) * 1024
|
|
|
|
end
|
|
|
|
private_class_method :max_email_sync_rss
|
|
|
|
|
|
|
|
DEFAULT_MAX_ALLOWED_EMAIL_SYNC_RSS_MEGABYTES = 500
|
|
|
|
|
|
|
|
def self.max_allowed_email_sync_rss
|
|
|
|
[
|
|
|
|
ENV["UNICORN_EMAIL_SYNC_MAX_RSS"].to_i,
|
|
|
|
DEFAULT_MAX_ALLOWED_EMAIL_SYNC_RSS_MEGABYTES,
|
|
|
|
].max.megabytes
|
|
|
|
end
|
|
|
|
private_class_method :max_allowed_email_sync_rss
|
|
|
|
|
|
|
|
if Rails.env.test?
|
|
|
|
def self.test_cleanup
|
|
|
|
@@email_sync_next_heartbeat_check = nil
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
def self.check_email_sync_heartbeat
|
|
|
|
if defined?(@@email_sync_next_heartbeat_check) && @@email_sync_next_heartbeat_check &&
|
|
|
|
@@email_sync_next_heartbeat_check > Time.now.to_i
|
|
|
|
return
|
|
|
|
end
|
|
|
|
|
|
|
|
@@email_sync_next_heartbeat_check = (Time.now + HEARTBEAT_INTERVAL).to_i
|
|
|
|
|
|
|
|
should_restart = false
|
|
|
|
|
|
|
|
# Restart process if it does not respond anymore
|
|
|
|
last_heartbeat_ago = Time.now.to_i - Discourse.redis.get(HEARTBEAT_KEY).to_i
|
|
|
|
|
|
|
|
if last_heartbeat_ago > HEARTBEAT_INTERVAL.to_i
|
|
|
|
Rails.logger.warn(
|
|
|
|
"EmailSync heartbeat test failed (last heartbeat was #{last_heartbeat_ago}s ago), restarting",
|
|
|
|
)
|
|
|
|
|
|
|
|
should_restart = true
|
|
|
|
end
|
|
|
|
|
|
|
|
# Restart process if memory usage is too high
|
|
|
|
if !should_restart && (email_sync_rss = max_email_sync_rss) > max_allowed_email_sync_rss
|
|
|
|
Rails.logger.warn(
|
|
|
|
"EmailSync is consuming too much memory (using: %0.2fM) for '%s', restarting" %
|
|
|
|
[(email_sync_rss.to_f / 1.megabyte), HOSTNAME],
|
|
|
|
)
|
|
|
|
|
|
|
|
should_restart = true
|
|
|
|
end
|
|
|
|
|
|
|
|
restart if should_restart
|
|
|
|
end
|
|
|
|
|
2020-07-10 05:05:55 -04:00
|
|
|
private
|
|
|
|
|
|
|
|
def suppress_stdout
|
|
|
|
false
|
|
|
|
end
|
|
|
|
|
|
|
|
def suppress_stderr
|
|
|
|
false
|
|
|
|
end
|
|
|
|
|
|
|
|
def start_thread(db, group)
|
|
|
|
Thread.new do
|
|
|
|
RailsMultisite::ConnectionManagement.with_connection(db) do
|
2021-01-20 20:37:47 -05:00
|
|
|
ImapSyncLog.debug("Thread started for group #{group.name} in db #{db}", group, db: false)
|
2020-07-10 05:05:55 -04:00
|
|
|
begin
|
2020-08-13 22:01:31 -04:00
|
|
|
syncer = Imap::Sync.new(group)
|
2020-07-10 05:05:55 -04:00
|
|
|
rescue Net::IMAP::NoResponseError => e
|
|
|
|
group.update(imap_last_error: e.message)
|
|
|
|
Thread.exit
|
|
|
|
end
|
|
|
|
|
2020-08-13 22:01:31 -04:00
|
|
|
@sync_lock.synchronize { @sync_data[db][group.id][:syncer] = syncer }
|
2020-07-10 05:05:55 -04:00
|
|
|
|
|
|
|
status = nil
|
|
|
|
idle = false
|
|
|
|
|
|
|
|
while @running && group.reload.imap_mailbox_name.present?
|
2020-08-13 22:01:31 -04:00
|
|
|
ImapSyncLog.debug("Processing mailbox for group #{group.name} in db #{db}", group)
|
|
|
|
status =
|
|
|
|
syncer.process(
|
|
|
|
idle: syncer.can_idle? && status && status[:remaining] == 0,
|
2020-07-10 05:05:55 -04:00
|
|
|
old_emails_limit: status && status[:remaining] > 0 ? 0 : nil,
|
|
|
|
)
|
|
|
|
|
2020-08-13 22:01:31 -04:00
|
|
|
if !syncer.can_idle? && status[:remaining] == 0
|
2021-01-20 20:37:47 -05:00
|
|
|
ImapSyncLog.debug(
|
|
|
|
"Going to sleep for group #{group.name} in db #{db} to wait for new emails",
|
|
|
|
group,
|
|
|
|
db: false,
|
|
|
|
)
|
2020-08-02 23:10:17 -04:00
|
|
|
|
2020-07-10 05:05:55 -04:00
|
|
|
# Thread goes into sleep for a bit so it is better to return any
|
|
|
|
# connection back to the pool.
|
|
|
|
ActiveRecord::Base.connection_handler.clear_active_connections!
|
|
|
|
|
|
|
|
sleep SiteSetting.imap_polling_period_mins.minutes
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2020-08-13 22:01:31 -04:00
|
|
|
syncer.disconnect!
|
2020-07-10 05:05:55 -04:00
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
def kill_threads
|
|
|
|
# This is not really safe so the caller should ensure it happens in a
|
|
|
|
# thread-safe context.
|
|
|
|
# It should be safe when called from within a `trap` (there are no
|
|
|
|
# synchronization primitives available anyway).
|
|
|
|
@running = false
|
|
|
|
|
|
|
|
@sync_data.each { |db, sync_data| sync_data.each { |_, data| kill_and_disconnect!(data) } }
|
|
|
|
|
|
|
|
exit 0
|
|
|
|
end
|
|
|
|
|
|
|
|
def after_fork
|
2024-06-03 00:51:12 -04:00
|
|
|
log("[EmailSync] Loading EmailSync in process id #{Process.pid}")
|
2020-07-10 05:05:55 -04:00
|
|
|
|
|
|
|
loop do
|
|
|
|
break if Discourse.redis.set(HEARTBEAT_KEY, Time.now.to_i, ex: HEARTBEAT_INTERVAL, nx: true)
|
|
|
|
sleep HEARTBEAT_INTERVAL
|
|
|
|
end
|
|
|
|
|
2024-06-03 00:51:12 -04:00
|
|
|
log("[EmailSync] Starting EmailSync main thread")
|
2020-07-10 05:05:55 -04:00
|
|
|
|
|
|
|
@running = true
|
|
|
|
@sync_data = {}
|
|
|
|
@sync_lock = Mutex.new
|
|
|
|
|
|
|
|
trap("INT") { kill_threads }
|
|
|
|
trap("TERM") { kill_threads }
|
|
|
|
trap("HUP") { kill_threads }
|
|
|
|
|
|
|
|
while @running
|
|
|
|
Discourse.redis.set(HEARTBEAT_KEY, Time.now.to_i, ex: HEARTBEAT_INTERVAL)
|
|
|
|
|
|
|
|
# Kill all threads for databases that no longer exist
|
|
|
|
all_dbs = Set.new(RailsMultisite::ConnectionManagement.all_dbs)
|
|
|
|
@sync_data.filter! do |db, sync_data|
|
|
|
|
next true if all_dbs.include?(db)
|
|
|
|
|
2020-09-09 23:41:46 -04:00
|
|
|
sync_data.each { |_, data| kill_and_disconnect!(data) }
|
2020-07-10 05:05:55 -04:00
|
|
|
|
|
|
|
false
|
|
|
|
end
|
|
|
|
|
|
|
|
RailsMultisite::ConnectionManagement.each_connection do |db|
|
2020-08-13 22:01:31 -04:00
|
|
|
next if !SiteSetting.enable_imap
|
2020-07-10 05:05:55 -04:00
|
|
|
|
2020-08-13 22:01:31 -04:00
|
|
|
groups = Group.with_imap_configured.map { |group| [group.id, group] }.to_h
|
2020-07-10 05:05:55 -04:00
|
|
|
|
2020-08-13 22:01:31 -04:00
|
|
|
@sync_lock.synchronize do
|
|
|
|
@sync_data[db] ||= {}
|
2020-07-10 05:05:55 -04:00
|
|
|
|
2020-08-13 22:01:31 -04:00
|
|
|
# Kill threads for group's mailbox that are no longer synchronized.
|
|
|
|
@sync_data[db].filter! do |group_id, data|
|
|
|
|
next true if groups[group_id] && data[:thread]&.alive? && !data[:syncer]&.disconnected?
|
2020-07-10 05:05:55 -04:00
|
|
|
|
2020-08-13 22:01:31 -04:00
|
|
|
if !groups[group_id]
|
|
|
|
ImapSyncLog.warn(
|
|
|
|
"Killing thread for group because mailbox is no longer synced",
|
|
|
|
group_id,
|
|
|
|
)
|
|
|
|
else
|
|
|
|
ImapSyncLog.warn("Thread for group is dead", group_id)
|
2020-07-10 05:05:55 -04:00
|
|
|
end
|
|
|
|
|
2020-09-09 23:41:46 -04:00
|
|
|
kill_and_disconnect!(data)
|
2020-08-13 22:01:31 -04:00
|
|
|
false
|
|
|
|
end
|
|
|
|
|
|
|
|
# Spawn new threads for groups that are now synchronized.
|
|
|
|
groups.each do |group_id, group|
|
|
|
|
if !@sync_data[db][group_id]
|
2021-01-20 20:37:47 -05:00
|
|
|
ImapSyncLog.debug(
|
|
|
|
"Starting thread for group #{group.name} mailbox #{group.imap_mailbox_name}",
|
|
|
|
group,
|
|
|
|
db: false,
|
|
|
|
)
|
2020-08-13 22:01:31 -04:00
|
|
|
|
|
|
|
@sync_data[db][group_id] = { thread: start_thread(db, group), syncer: nil }
|
2020-07-10 05:05:55 -04:00
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
# Thread goes into sleep for a bit so it is better to return any
|
|
|
|
# connection back to the pool.
|
|
|
|
ActiveRecord::Base.connection_handler.clear_active_connections!
|
|
|
|
|
|
|
|
sleep 5
|
|
|
|
end
|
|
|
|
|
|
|
|
@sync_lock.synchronize { kill_threads }
|
|
|
|
Discourse.redis.del(HEARTBEAT_KEY)
|
|
|
|
exit 0
|
|
|
|
rescue => e
|
2024-06-03 00:51:12 -04:00
|
|
|
log("#{e.message}: #{e.backtrace.join("\n")}")
|
2020-07-10 05:05:55 -04:00
|
|
|
exit 1
|
|
|
|
end
|
2020-09-09 23:41:46 -04:00
|
|
|
|
|
|
|
def kill_and_disconnect!(data)
|
|
|
|
data[:thread].kill
|
|
|
|
data[:thread].join
|
2024-06-03 00:51:12 -04:00
|
|
|
|
2020-09-09 23:41:46 -04:00
|
|
|
begin
|
|
|
|
data[:syncer]&.disconnect!
|
|
|
|
rescue Net::IMAP::ResponseError => err
|
2024-06-03 00:51:12 -04:00
|
|
|
log(
|
|
|
|
"[EmailSync] Encountered a response error when disconnecting: #{err}\n#{err.backtrace.join("\n")}",
|
|
|
|
)
|
2020-09-09 23:41:46 -04:00
|
|
|
end
|
|
|
|
end
|
2020-07-10 05:05:55 -04:00
|
|
|
end
|