mirror of
https://github.com/l5yth/potato-mesh.git
synced 2026-07-06 01:42:11 +02:00
c8d5a8a20f
* web: fix pubsub thread budget to not block entire app * web: address review comments
293 lines
10 KiB
Ruby
293 lines
10 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
|
|
|
|
require "sinatra/base"
|
|
require "json"
|
|
require "sqlite3"
|
|
require "fileutils"
|
|
require "logger"
|
|
require "rack/utils"
|
|
require "open3"
|
|
require "resolv"
|
|
require "socket"
|
|
require "time"
|
|
require "openssl"
|
|
require "base64"
|
|
require "prometheus/client"
|
|
require "prometheus/client/formats/text"
|
|
require "prometheus/middleware/collector"
|
|
require "prometheus/middleware/exporter"
|
|
require "net/http"
|
|
require "uri"
|
|
require "ipaddr"
|
|
require "set"
|
|
require "digest"
|
|
|
|
require_relative "config"
|
|
require_relative "sanitizer"
|
|
require_relative "meta"
|
|
require_relative "logging"
|
|
require_relative "og_image"
|
|
require_relative "application/helpers"
|
|
require_relative "application/errors"
|
|
require_relative "application/database"
|
|
require_relative "application/networking"
|
|
require_relative "application/identity"
|
|
require_relative "application/worker_pool"
|
|
require_relative "application/federation"
|
|
require_relative "application/prometheus"
|
|
require_relative "application/queries"
|
|
require_relative "application/meshtastic/channel_names"
|
|
require_relative "application/meshtastic/channel_hash"
|
|
require_relative "application/meshtastic/protobuf"
|
|
require_relative "application/meshtastic/rainbow_table"
|
|
require_relative "application/meshtastic/cipher"
|
|
require_relative "application/meshtastic/payload_decoder"
|
|
require_relative "application/data_processing"
|
|
require_relative "application/filesystem"
|
|
require_relative "application/api_cache"
|
|
require_relative "application/pubsub"
|
|
require_relative "application/pages"
|
|
require_relative "application/instances"
|
|
require_relative "application/retention"
|
|
require_relative "application/routes/api"
|
|
require_relative "application/routes/events"
|
|
require_relative "application/routes/ingest"
|
|
require_relative "application/routes/root"
|
|
|
|
module PotatoMesh
|
|
class Application < Sinatra::Base
|
|
extend App::Helpers
|
|
extend App::Database
|
|
extend App::Networking
|
|
extend App::Identity
|
|
extend App::Federation
|
|
extend App::Instances
|
|
extend App::Prometheus
|
|
extend App::Queries
|
|
extend App::DataProcessing
|
|
extend App::Filesystem
|
|
extend App::Pages
|
|
extend App::Retention
|
|
|
|
helpers App::Helpers
|
|
include App::Database
|
|
include App::Networking
|
|
include App::Identity
|
|
include App::Federation
|
|
include App::Instances
|
|
include App::Prometheus
|
|
include App::Queries
|
|
include App::DataProcessing
|
|
include App::Filesystem
|
|
include App::Pages
|
|
include App::Retention
|
|
|
|
register App::Routes::Api
|
|
register App::Routes::Events
|
|
register App::Routes::Ingest
|
|
register App::Routes::Root
|
|
|
|
DEFAULT_PORT = 41_447
|
|
DEFAULT_BIND_ADDRESS = "0.0.0.0"
|
|
|
|
APP_VERSION = determine_app_version
|
|
INSTANCE_PRIVATE_KEY, INSTANCE_KEY_GENERATED = load_or_generate_instance_private_key
|
|
INSTANCE_PUBLIC_KEY_PEM = INSTANCE_PRIVATE_KEY.public_key.export
|
|
SELF_INSTANCE_ID = Digest::SHA256.hexdigest(INSTANCE_PUBLIC_KEY_PEM)
|
|
INSTANCE_DOMAIN, INSTANCE_DOMAIN_SOURCE = determine_instance_domain
|
|
|
|
# Adjust the runtime logger severity to match the DEBUG flag.
|
|
#
|
|
# @return [void]
|
|
def self.apply_logger_level!
|
|
logger = settings.logger
|
|
return unless logger
|
|
|
|
logger.level = PotatoMesh::Config.debug? ? Logger::DEBUG : Logger::INFO
|
|
end
|
|
|
|
# Close every live-update SSE subscriber so their streaming +pump+ loops
|
|
# exit promptly. Invoked from the shutdown signal handler (idempotent) so
|
|
# an open +/api/events+ connection cannot block Puma's graceful shutdown,
|
|
# which would otherwise gate the at_exit federation/retention teardown.
|
|
#
|
|
# @return [void]
|
|
def self.close_live_update_subscribers!
|
|
PotatoMesh::App::PubSub.reset!
|
|
rescue StandardError
|
|
nil
|
|
end
|
|
|
|
# Install INT/TERM handlers that close the live-update SSE subscribers when
|
|
# the process is asked to stop, so the streaming requests end and Puma can
|
|
# shut down without waiting out the SSE lifetime. The handler spawns a
|
|
# thread because the close path takes a Mutex, which is unsafe directly in
|
|
# trap context. Sinatra's own trap (installed by +run!+) chains to whatever
|
|
# handler already exists, so installing this *before* +run!+ keeps both.
|
|
#
|
|
# @param signals [Array<Symbol>] signals to trap.
|
|
# @param trap [#call] trap installer (injected in tests).
|
|
# @return [void]
|
|
def self.install_pubsub_shutdown_signal_handlers!(signals: %i[INT TERM], trap: Signal.method(:trap))
|
|
signals.each do |signal|
|
|
trap.call(signal) { Thread.new { close_live_update_subscribers! } }
|
|
end
|
|
end
|
|
|
|
# Boot the HTTP server: install the SSE-shutdown signal handlers (so Ctrl+C
|
|
# closes the live-update streams and Puma can drain) and then hand off to
|
|
# Sinatra's +run!+. Factored out of +app.rb+ so the bootstrap wiring is unit
|
|
# testable without actually starting Puma.
|
|
#
|
|
# @return [void]
|
|
def self.run_server!
|
|
install_pubsub_shutdown_signal_handlers!
|
|
run!
|
|
end
|
|
|
|
# Determine the port the application should listen on by honouring the
|
|
# conventional +PORT+ environment variable used by hosting platforms. Any
|
|
# non-numeric or out-of-range values fall back to the provided default to
|
|
# keep the application bootable in misconfigured environments.
|
|
#
|
|
# @param default_port [Integer] fallback port when +ENV['PORT']+ is absent or invalid.
|
|
# @return [Integer] port number for the HTTP server.
|
|
def self.resolve_port(default_port: DEFAULT_PORT)
|
|
raw_port = ENV["PORT"]
|
|
return default_port if raw_port.nil?
|
|
|
|
trimmed = raw_port.to_s.strip
|
|
return default_port if trimmed.empty?
|
|
|
|
begin
|
|
port = Integer(trimmed, 10)
|
|
rescue ArgumentError
|
|
return default_port
|
|
end
|
|
|
|
return default_port unless port.positive?
|
|
return default_port unless PotatoMesh::Sanitizer.valid_port?(trimmed)
|
|
|
|
port
|
|
end
|
|
|
|
configure do
|
|
set :public_folder, File.expand_path("../../public", __dir__)
|
|
set :views, File.expand_path("../../views", __dir__)
|
|
set :federation_thread, nil
|
|
set :initial_federation_thread, nil
|
|
set :federation_worker_pool, nil
|
|
set :federation_shutdown_requested, false
|
|
set :federation_shutdown_hook_installed, false
|
|
set :retention_thread, nil
|
|
set :retention_shutdown_requested, false
|
|
set :retention_shutdown_hook_installed, false
|
|
set :port, resolve_port
|
|
set :bind, DEFAULT_BIND_ADDRESS
|
|
# Size the Puma worker-thread pool in code (default 16:96, env
|
|
# MIN_THREADS/MAX_THREADS). Each /api/events SSE stream pins one request
|
|
# thread for its lifetime, so the pool is kept well above the SSE
|
|
# subscriber cap (PubSub::MAX_SUBSCRIBERS) plus a reserve, ensuring live
|
|
# updates can never starve API/ingest/federation traffic (SPEC PS9);
|
|
# Puma's MRI default of 5 is far too small for that. force_shutdown_after
|
|
# bounds Puma's graceful-shutdown wait so a long-lived SSE stream cannot
|
|
# block Ctrl+C (the signal handler closes subscribers; this backstops
|
|
# anything still in flight after the window).
|
|
set :server_settings, {
|
|
force_shutdown_after: PotatoMesh::Config.puma_force_shutdown_seconds,
|
|
Threads: PotatoMesh::Config.puma_threads_setting,
|
|
}
|
|
|
|
app_logger = PotatoMesh::Logging.build_logger($stdout)
|
|
set :logger, app_logger
|
|
use Rack::CommonLogger, app_logger
|
|
use Rack::Deflater
|
|
use ::Prometheus::Middleware::Collector
|
|
use ::Prometheus::Middleware::Exporter
|
|
|
|
apply_logger_level!
|
|
|
|
perform_initial_filesystem_setup!
|
|
cleanup_legacy_well_known_artifacts
|
|
ensure_schema_upgrades
|
|
init_db unless db_schema_present?
|
|
|
|
log_instance_domain_resolution
|
|
log_instance_public_key
|
|
refresh_well_known_document_if_stale
|
|
ensure_self_instance_record!
|
|
update_all_prometheus_metrics_from_nodes
|
|
|
|
start_retention_worker_if_active!
|
|
|
|
if federation_enabled?
|
|
ensure_federation_worker_pool!
|
|
else
|
|
shutdown_federation_worker_pool!
|
|
end
|
|
|
|
if federation_announcements_active?
|
|
info_log(
|
|
"Federation enabled",
|
|
context: "federation",
|
|
seed_count: PotatoMesh::Config.federation_seed_domains.length,
|
|
announcement_interval_seconds: PotatoMesh::Config.federation_announcement_interval,
|
|
worker_pool_size: PotatoMesh::Config.federation_worker_pool_size,
|
|
)
|
|
start_initial_federation_announcement!
|
|
start_federation_announcer!
|
|
elsif federation_enabled?
|
|
debug_log(
|
|
"Federation announcements disabled",
|
|
context: "federation",
|
|
reason: "test environment",
|
|
)
|
|
else
|
|
info_log(
|
|
"Federation disabled",
|
|
context: "federation",
|
|
reason: "configuration",
|
|
)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
if defined?(Sinatra::Application) && Sinatra::Application != PotatoMesh::Application
|
|
Sinatra.send(:remove_const, :Application)
|
|
end
|
|
Sinatra::Application = PotatoMesh::Application unless defined?(Sinatra::Application)
|
|
|
|
APP_VERSION = PotatoMesh::Application::APP_VERSION unless defined?(APP_VERSION)
|
|
SELF_INSTANCE_ID = PotatoMesh::Application::SELF_INSTANCE_ID unless defined?(SELF_INSTANCE_ID)
|
|
|
|
[
|
|
PotatoMesh::App::Helpers,
|
|
PotatoMesh::App::Database,
|
|
PotatoMesh::App::Networking,
|
|
PotatoMesh::App::Identity,
|
|
PotatoMesh::App::Federation,
|
|
PotatoMesh::App::Instances,
|
|
PotatoMesh::App::Prometheus,
|
|
PotatoMesh::App::Queries,
|
|
PotatoMesh::App::DataProcessing,
|
|
PotatoMesh::App::Pages,
|
|
PotatoMesh::App::Retention,
|
|
].each do |mod|
|
|
Object.include(mod) unless Object < mod
|
|
end
|