mirror of
https://github.com/l5yth/potato-mesh.git
synced 2026-07-06 01:42:11 +02:00
205 lines
7.9 KiB
Ruby
205 lines
7.9 KiB
Ruby
# Copyright © 2025-26 l5yth & contributors
|
|
#
|
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
# you may not use this file except in compliance with the License.
|
|
# You may obtain a copy of the License at
|
|
#
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
#
|
|
# Unless required by applicable law or agreed to in writing, software
|
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
# See the License for the specific language governing permissions and
|
|
# limitations under the License.
|
|
|
|
# frozen_string_literal: true
|
|
|
|
module PotatoMesh
|
|
module App
|
|
module Federation
|
|
# Maximum slice (seconds) used by +federation_sleep_with_shutdown+ when
|
|
# decomposing a target sleep into shutdown-aware increments.
|
|
FEDERATION_SLEEP_SLICE_SECONDS = 0.2
|
|
|
|
# Retrieve or initialize the worker pool servicing federation jobs.
|
|
#
|
|
# @return [PotatoMesh::App::WorkerPool, nil] active worker pool or nil when disabled.
|
|
def federation_worker_pool
|
|
ensure_federation_worker_pool!
|
|
end
|
|
|
|
# Ensure the federation worker pool exists when federation remains enabled.
|
|
#
|
|
# Threading model: the pool is a fixed-size thread pool backed by a bounded
|
|
# queue. A single long-lived announcer thread (started by
|
|
# {#start_federation_announcer!}) drives periodic crawl and announcement
|
|
# cycles by submitting tasks onto the pool; individual crawl and announce
|
|
# jobs then run concurrently on pool threads. The pool is lazily
|
|
# instantiated on first use and is memoized on the Sinatra settings object so
|
|
# that all requests share the same instance. An +at_exit+ hook
|
|
# ({#ensure_federation_shutdown_hook!}) guarantees the pool drains cleanly on
|
|
# process termination even when the announcer thread is still alive.
|
|
#
|
|
# @return [PotatoMesh::App::WorkerPool, nil] active worker pool if created.
|
|
def ensure_federation_worker_pool!
|
|
return nil unless federation_enabled?
|
|
return nil if federation_shutdown_requested?
|
|
|
|
ensure_federation_shutdown_hook!
|
|
|
|
existing = settings.respond_to?(:federation_worker_pool) ? settings.federation_worker_pool : nil
|
|
return existing if existing&.alive?
|
|
|
|
pool = PotatoMesh::App::WorkerPool.new(
|
|
size: PotatoMesh::Config.federation_worker_pool_size,
|
|
max_queue: PotatoMesh::Config.federation_worker_queue_capacity,
|
|
task_timeout: PotatoMesh::Config.federation_task_timeout_seconds,
|
|
name: "potato-mesh-fed",
|
|
)
|
|
|
|
set(:federation_worker_pool, pool) if respond_to?(:set)
|
|
pool
|
|
end
|
|
|
|
# Ensure federation background workers are torn down during process exit.
|
|
#
|
|
# @return [void]
|
|
def ensure_federation_shutdown_hook!
|
|
application = is_a?(Class) ? self : self.class
|
|
return application.ensure_federation_shutdown_hook! unless application.equal?(self)
|
|
|
|
installed = if respond_to?(:settings) && settings.respond_to?(:federation_shutdown_hook_installed)
|
|
settings.federation_shutdown_hook_installed
|
|
else
|
|
instance_variable_defined?(:@federation_shutdown_hook_installed) && @federation_shutdown_hook_installed
|
|
end
|
|
return if installed
|
|
|
|
if respond_to?(:set) && settings.respond_to?(:federation_shutdown_hook_installed=)
|
|
set(:federation_shutdown_hook_installed, true)
|
|
else
|
|
@federation_shutdown_hook_installed = true
|
|
end
|
|
|
|
at_exit do
|
|
begin
|
|
application.shutdown_federation_background_work!(timeout: PotatoMesh::Config.federation_shutdown_timeout_seconds)
|
|
rescue StandardError
|
|
# Suppress shutdown errors during interpreter teardown.
|
|
end
|
|
end
|
|
end
|
|
|
|
# Check whether federation workers have received a shutdown request.
|
|
#
|
|
# @return [Boolean] true when stop has been requested.
|
|
def federation_shutdown_requested?
|
|
return false unless respond_to?(:settings)
|
|
return false unless settings.respond_to?(:federation_shutdown_requested)
|
|
|
|
settings.federation_shutdown_requested == true
|
|
end
|
|
|
|
# Mark federation background work as shutting down.
|
|
#
|
|
# @return [void]
|
|
def request_federation_shutdown!
|
|
set(:federation_shutdown_requested, true) if respond_to?(:set)
|
|
end
|
|
|
|
# Clear any previously requested federation shutdown marker.
|
|
#
|
|
# @return [void]
|
|
def clear_federation_shutdown_request!
|
|
set(:federation_shutdown_requested, false) if respond_to?(:set)
|
|
end
|
|
|
|
# Sleep in short intervals so federation loops can react to shutdown.
|
|
#
|
|
# @param seconds [Numeric] target sleep duration.
|
|
# @return [Boolean] true when the full delay elapsed without shutdown.
|
|
def federation_sleep_with_shutdown(seconds)
|
|
remaining = seconds.to_f
|
|
while remaining.positive?
|
|
return false if federation_shutdown_requested?
|
|
|
|
slice = [remaining, FEDERATION_SLEEP_SLICE_SECONDS].min
|
|
Kernel.sleep(slice)
|
|
remaining -= slice
|
|
end
|
|
!federation_shutdown_requested?
|
|
end
|
|
|
|
# Shutdown and clear the federation worker pool if present.
|
|
#
|
|
# Bounded by +federation_shutdown_timeout_seconds+ (default 3s), not the
|
|
# per-task timeout — operators expect CTRL+C to reap within seconds, not
|
|
# to wait out a long-running federation cycle's worst case.
|
|
#
|
|
# @return [void]
|
|
def shutdown_federation_worker_pool!
|
|
existing = settings.respond_to?(:federation_worker_pool) ? settings.federation_worker_pool : nil
|
|
return unless existing
|
|
|
|
begin
|
|
shutdown_timeout = PotatoMesh::Config.federation_shutdown_timeout_seconds
|
|
existing.shutdown(
|
|
timeout: shutdown_timeout,
|
|
force_kill_after: shutdown_timeout,
|
|
)
|
|
rescue StandardError => e
|
|
warn_log(
|
|
"Failed to shut down federation worker pool",
|
|
context: "federation",
|
|
error_class: e.class.name,
|
|
error_message: e.message,
|
|
)
|
|
ensure
|
|
set(:federation_worker_pool, nil) if respond_to?(:set)
|
|
end
|
|
end
|
|
|
|
# Gracefully terminate federation background loops and worker pool tasks.
|
|
#
|
|
# @param timeout [Numeric, nil] maximum join time applied per thread.
|
|
# @return [void]
|
|
def shutdown_federation_background_work!(timeout: nil)
|
|
request_federation_shutdown!
|
|
timeout_value = timeout || PotatoMesh::Config.federation_shutdown_timeout_seconds
|
|
# Drain the worker pool first so federation threads blocked in
|
|
# wait_for_federation_tasks unblock promptly instead of waiting
|
|
# for each task's individual timeout to expire.
|
|
shutdown_federation_worker_pool!
|
|
stop_federation_thread!(:initial_federation_thread, timeout: timeout_value)
|
|
stop_federation_thread!(:federation_thread, timeout: timeout_value)
|
|
clear_federation_crawl_state!
|
|
end
|
|
|
|
# Stop a specific federation thread setting and clear its reference.
|
|
#
|
|
# @param setting_name [Symbol] settings key storing the thread object.
|
|
# @param timeout [Numeric] seconds to wait for clean thread exit.
|
|
# @return [void]
|
|
def stop_federation_thread!(setting_name, timeout:)
|
|
return unless respond_to?(:settings)
|
|
return unless settings.respond_to?(setting_name)
|
|
|
|
thread = settings.public_send(setting_name)
|
|
if thread&.alive?
|
|
begin
|
|
thread.wakeup if thread.respond_to?(:wakeup)
|
|
rescue ThreadError
|
|
# The thread may not currently be sleeping; continue shutdown.
|
|
end
|
|
thread.join(timeout)
|
|
if thread.alive?
|
|
thread.kill
|
|
thread.join(0.1)
|
|
end
|
|
end
|
|
set(setting_name, nil) if respond_to?(:set)
|
|
end
|
|
end
|
|
end
|
|
end
|