diff --git a/bake/falcon/supervisor.rb b/bake/falcon/supervisor.rb index 1aad062..ad73c6c 100644 --- a/bake/falcon/supervisor.rb +++ b/bake/falcon/supervisor.rb @@ -4,7 +4,5 @@ # Copyright, 2020-2025, by Samuel Williams. def restart - require_relative "../../lib/falcon/command/supervisor" - - Falcon::Command::Supervisor["restart"].call + context.lookup("async:container:supervisor:restart").call end diff --git a/examples/hello/falcon.rb b/examples/hello/falcon.rb index b8318e3..ea6e757 100755 --- a/examples/hello/falcon.rb +++ b/examples/hello/falcon.rb @@ -21,7 +21,7 @@ # append preload "preload.rb" - + include Async::Container::Supervisor::Supervised end service "supervisor" do diff --git a/falcon.gemspec b/falcon.gemspec index a31d9ca..e18d555 100644 --- a/falcon.gemspec +++ b/falcon.gemspec @@ -28,13 +28,13 @@ Gem::Specification.new do |spec| spec.add_dependency "async" spec.add_dependency "async-container", "~> 0.20" + spec.add_dependency "async-container-supervisor", "~> 0.4.0" spec.add_dependency "async-http", "~> 0.75" spec.add_dependency "async-http-cache", "~> 0.4" spec.add_dependency "async-service", "~> 0.10" spec.add_dependency "bundler" spec.add_dependency "localhost", "~> 1.1" spec.add_dependency "openssl", "~> 3.0" - spec.add_dependency "process-metrics", "~> 0.2" spec.add_dependency "protocol-http", "~> 0.31" spec.add_dependency "protocol-rack", "~> 0.7" spec.add_dependency "samovar", "~> 2.3" diff --git a/lib/falcon/command/supervisor.rb b/lib/falcon/command/supervisor.rb deleted file mode 100644 index 99d6924..0000000 --- a/lib/falcon/command/supervisor.rb +++ /dev/null @@ -1,73 +0,0 @@ -# frozen_string_literal: true - -# Released under the MIT License. -# Copyright, 2019-2024, by Samuel Williams. - -require "samovar" -require "async" -require "json" - -require "io/endpoint/unix_endpoint" -require "io/stream" - -module Falcon - module Command - # Implements the `falcon supervisor` command. - # - # Talks to an instance of the supervisor to issue commands and print results. - class Supervisor < Samovar::Command - self.description = "Control and query a specific supervisor." - - # The command line options. - # @attribute [Samovar::Options] - options do - option "--path ", "The control IPC path.", default: "supervisor.ipc" - end - - # Implements the `falcon supervisor restart` command. - class Restart < Samovar::Command - self.description = "Restart the process group." - - # Send the restart message to the supervisor. - def call(stream) - stream.puts({please: "restart"}.to_json, separator: "\0") - end - end - - # Implements the `falcon supervisor metrics` command. - class Metrics < Samovar::Command - self.description = "Show metrics about the falcon processes." - - # Send the metrics message to the supervisor and print the results. - def call(stream) - stream.puts({please: "metrics"}.to_json, separator: "\0", chomp: true) - response = JSON.parse(stream.read_until("\0"), symbolize_names: true) - - $stdout.puts response - end - end - - # The nested command to execute. - # @name nested - # @attribute [Command] - nested :command, { - "restart" => Restart, - "metrics" => Metrics, - }, default: "metrics" - - # The endpoint the supervisor is bound to. - def endpoint - ::IO::Endpoint.unix(@options[:path]) - end - - # Connect to the supervisor and execute the requested command. - def call - Sync do - endpoint.connect do |socket| - @command.call(IO::Stream(socket)) - end - end - end - end - end -end diff --git a/lib/falcon/command/top.rb b/lib/falcon/command/top.rb index 6d7ae17..44bdc31 100644 --- a/lib/falcon/command/top.rb +++ b/lib/falcon/command/top.rb @@ -8,7 +8,6 @@ require_relative "virtual" require_relative "proxy" require_relative "redirect" -require_relative "supervisor" require_relative "../version" @@ -38,7 +37,6 @@ class Top < Samovar::Command "virtual" => Virtual, "proxy" => Proxy, "redirect" => Redirect, - "supervisor" => Supervisor, }, default: "serve" # Whether verbose logging is enabled. diff --git a/lib/falcon/environment/supervisor.rb b/lib/falcon/environment/supervisor.rb index c10f3b0..efbb51a 100644 --- a/lib/falcon/environment/supervisor.rb +++ b/lib/falcon/environment/supervisor.rb @@ -3,42 +3,18 @@ # Released under the MIT License. # Copyright, 2019-2024, by Samuel Williams. -require_relative "../service/supervisor" require_relative "../environment" -require "io/endpoint/unix_endpoint" +require "async/container/supervisor" module Falcon module Environment # Provides an environment for hosting a supervisor which can monitor multiple applications. module Supervisor - # The service class to use for the supervisor. - # @returns [Class] - def service_class - ::Falcon::Service::Supervisor - end - - # The name of the supervisor - # @returns [String] - def name - "supervisor" - end - - # The IPC path to use for communication with the supervisor. - # @returns [String] - def ipc_path - ::File.expand_path("supervisor.ipc", root) - end - - # The endpoint the supervisor will bind to. - # @returns [::IO::Endpoint::Generic] - def endpoint - ::IO::Endpoint.unix(ipc_path) - end + include Async::Container::Supervisor::Environment - # Options to use when creating the container. - def container_options - {restart: true, count: 1, health_check_timeout: 30} + def monitors + [Async::Container::Supervisor::MemoryMonitor.new(interval: 10)] end end diff --git a/lib/falcon/service/server.rb b/lib/falcon/service/server.rb index 42b064d..548fd24 100644 --- a/lib/falcon/service/server.rb +++ b/lib/falcon/service/server.rb @@ -5,6 +5,7 @@ # Copyright, 2020, by Daniel Evans. require "async/service/generic" +require "async/container/supervisor/supervised" require "async/http/endpoint" require_relative "../server" @@ -58,6 +59,10 @@ def setup(container) evaluator = @environment.evaluator Async do |task| + if @environment.implements?(Async::Container::Supervisor::Supervised) + evaluator.make_supervised_worker(instance).run + end + server = evaluator.make_server(@bound_endpoint) server.run diff --git a/lib/falcon/service/supervisor.rb b/lib/falcon/service/supervisor.rb deleted file mode 100644 index 7c3421a..0000000 --- a/lib/falcon/service/supervisor.rb +++ /dev/null @@ -1,123 +0,0 @@ -# frozen_string_literal: true - -# Released under the MIT License. -# Copyright, 2019-2024, by Samuel Williams. - -require "process/metrics" -require "json" - -require "async/service/generic" - -require "io/endpoint/bound_endpoint" -require "io/stream" - -module Falcon - module Service - # Implements a host supervisor which can restart the host services and provide various metrics about the running processes. - class Supervisor < Async::Service::Generic - # Initialize the supervisor using the given environment. - # @parameter environment [Build::Environment] - def initialize(...) - super - - @bound_endpoint = nil - end - - # The endpoint which the supervisor will bind to. - # Typically a unix pipe in the same directory as the host. - def endpoint - @evaluator.endpoint - end - - # Restart the process group that the supervisor belongs to. - def do_restart(message) - # Tell the parent of this process group to spin up a new process group/container. - # Wait for that to start accepting new connections. - # Stop accepting connections. - # Wait for existing connnections to drain. - # Terminate this process group. - signal = message[:signal] || :INT - - Process.kill(signal, Process.ppid) - end - - # Capture process metrics relating to the process group that the supervisor belongs to. - def do_metrics(message) - Process::Metrics::General.capture(pid: Process.ppid, ppid: Process.ppid) - end - - # Handle an incoming request. - # @parameter message [Hash] The decoded message. - def handle(message) - case message[:please] - when "restart" - self.do_restart(message) - when "metrics" - self.do_metrics(message) - end - end - - # Bind the supervisor to the specified endpoint. - def start - Console.logger.info(self) {"Binding to #{self.endpoint}..."} - - @bound_endpoint = Sync{self.endpoint.bound} - - super - end - - # Start the supervisor process which accepts connections from the bound endpoint and processes JSON formatted messages. - # @parameter container [Async::Container::Generic] - def setup(container) - container_options = @evaluator.container_options - health_check_timeout = container_options[:health_check_timeout] - - container.run(name: self.name, **container_options) do |instance| - Async do - @bound_endpoint.accept do |peer| - stream = ::IO::Stream(peer) - - while message = stream.read_until("\0") - response = handle(JSON.parse(message, symbolize_names: true)) - stream.puts(response.to_json, separator: "\0") - end - end - - instance.ready! - - if health_check_timeout - Async(transient: true) do - while true - sleep(health_check_timeout / 2) - instance.ready! - end - end - end - end - end - - super - end - - # Release the bound endpoint. - def stop - @bound_endpoint&.close - @bound_endpoint = nil - - super - end - - def invoke(command) - @bound_endpoint.local_address_endpoint.connect do |peer| - stream = ::IO::Stream(peer) - - stream.puts(command.to_json, separator: "\0") - - response = JSON.parse(stream.read_until("\0"), symbolize_names: true) - - return response - end - end - end - end -end diff --git a/releases.md b/releases.md index 310d350..2262d0f 100644 --- a/releases.md +++ b/releases.md @@ -4,6 +4,33 @@ - Introduce {ruby Falcon::Environment::Server#make_server} which gives you full control over the server creation process. +### Introduce `Async::Container::Supervisor`. + +`Async::Container::Supervisor` is a new supervisor implementation that replaces Falcon's own supervisor. This allows you to use the same supervisor for all your services, and provides a more consistent interface for managing services. The supervisor is now a separate gem, `async-container-supervisor`. + +By default, the supervisor does not perform any monitoring, but you may add monitoring by defining them in the service definition. For example: + +``` ruby +service "hello.localhost" do + # Configure server... + + include Async::Container::Supervisor::Supervised +end + +service "supervisor" do + include Async::Container::Supervisor::Environment + + monitors do + [ + # Limit total memory usage to 512MiB: + Async::Container::Supervisor::MemoryMonitor.new(interval: 10, limit: 1024 * 1024 * 512), + ] + end +end +``` + +We retain the `falcon:supervisor:restart` task, but you may prefer to use `async:container:supervisor:restart` directly. + ## v0.50.0 - Add {ruby Falcon::Environment::Server#endpoint_options} to allow configuration of the endpoint options more easily. diff --git a/test/falcon/service/supervisor.rb b/test/falcon/environment/supervisor.rb similarity index 58% rename from test/falcon/service/supervisor.rb rename to test/falcon/environment/supervisor.rb index 18c8d84..c7f3a5f 100644 --- a/test/falcon/service/supervisor.rb +++ b/test/falcon/environment/supervisor.rb @@ -3,13 +3,12 @@ # Released under the MIT License. # Copyright, 2024, by Samuel Williams. -require "falcon/service/supervisor" require "falcon/configuration" require "falcon/environment/supervisor" require "temporary_directory_context" -describe Falcon::Service::Supervisor do +describe Falcon::Environment::Supervisor do include TemporaryDirectoryContext let(:environment) do @@ -19,11 +18,7 @@ end let(:supervisor) do - subject.new(environment) - end - - it "can create a supervisor" do - expect(supervisor).to be_a subject + environment.evaluator.service_class.new(environment) end it "can start and stop server" do @@ -35,14 +30,20 @@ expect(container.group.running).to have_attributes(size: be == 1) - response = supervisor.invoke({please: "metrics"}) - - expect(response).to be_a(Hash) - - # The supervisor should report itself: - expect(response.values).to have_value(have_keys( - command: be == "supervisor" - )) + Sync do + client = Async::Container::Supervisor::Client.new(endpoint: environment.evaluator.endpoint) + client.connect do |connection| + response = connection.call(do: "status") + + expect(response).to be_a(Array) + expect(response.size).to be == 1 + + first = response.first + expect(first).to have_keys( + memory_monitor: be_a(Hash), + ) + end + end ensure supervisor.stop container.stop