From 2078f0a6b56c1ab4511bce0f3379e901790b3921 Mon Sep 17 00:00:00 2001 From: Samuel Williams Date: Mon, 4 Aug 2025 09:49:58 +1200 Subject: [PATCH 1/6] Better integration with kubernetes. --- bake/async/container/notify/log.rb | 23 +++++++++++++++ guides/getting-started/readme.md | 22 -------------- guides/kubernetes-integration/readme.md | 39 +++++++++++++++++++++++++ guides/links.yaml | 8 +++++ guides/systemd-integration/readme.md | 22 ++++++++++++++ lib/async/container/notify/log.rb | 6 +++- releases.md | 4 +++ 7 files changed, 101 insertions(+), 23 deletions(-) create mode 100644 bake/async/container/notify/log.rb create mode 100644 guides/kubernetes-integration/readme.md create mode 100644 guides/links.yaml create mode 100644 guides/systemd-integration/readme.md diff --git a/bake/async/container/notify/log.rb b/bake/async/container/notify/log.rb new file mode 100644 index 0000000..6b4af28 --- /dev/null +++ b/bake/async/container/notify/log.rb @@ -0,0 +1,23 @@ + +def initialize(...) + super + + require "async/container/notify/log" +end + +# Check if the log file exists and the service is ready. +def ready?(path: Async::Container::Notify::Log.path) + if File.exist?(path) + File.foreach(path) do |line| + message = JSON.parse(line) + if message["ready"] == true + $stderr.puts "Service is ready: #{line}" + return true + end + end + + raise "Service is not ready yet." + else + raise "Notification log file does not exist at #{path}" + end +end diff --git a/guides/getting-started/readme.md b/guides/getting-started/readme.md index 7154ceb..32fa693 100644 --- a/guides/getting-started/readme.md +++ b/guides/getting-started/readme.md @@ -85,25 +85,3 @@ controller.run `SIGKILL` is the kill signal. The only behavior is to kill the process, immediately. As the process cannot catch the signal, it cannot cleanup, and thus this is a signal of last resort. `SIGSTOP` is the pause signal. The only behavior is to pause the process; the signal cannot be caught or ignored. The shell uses pausing (and its counterpart, resuming via `SIGCONT`) to implement job control. - -## Integration - -### systemd - -Install a template file into `/etc/systemd/system/`: - -``` -# my-daemon.service -[Unit] -Description=My Daemon -AssertPathExists=/srv/ - -[Service] -Type=notify -WorkingDirectory=/srv/my-daemon -ExecStart=bundle exec my-daemon -Nice=5 - -[Install] -WantedBy=multi-user.target -``` diff --git a/guides/kubernetes-integration/readme.md b/guides/kubernetes-integration/readme.md new file mode 100644 index 0000000..8c25082 --- /dev/null +++ b/guides/kubernetes-integration/readme.md @@ -0,0 +1,39 @@ +# Kubernetes Integration + +This guide explains how to use `async-container` with Kubernetes to manage your application as a containerized service. + +## Deployment Configuration + +Create a deployment configuration file for your application: + +```yaml +# my-app-deployment.yaml +apiVersion: apps/v1 +kind: Deployment +metadata: + name: my-app +spec: + replicas: 1 + selector: + matchLabels: + app: my-app + template: + metadata: + labels: + app: my-app + spec: + containers: + - name: my-app + image: my-app-image:latest + env: + - name: NOTIFY_LOG + value: "/tmp/notify.log" + ports: + - containerPort: 3000 + readinessProbe: + exec: + command: ["bundle", "exec", "bake", "async:container:notify:log:ready?"] + initialDelaySeconds: 5 + periodSeconds: 5 + failureThreshold: 12 +``` diff --git a/guides/links.yaml b/guides/links.yaml new file mode 100644 index 0000000..a59f55f --- /dev/null +++ b/guides/links.yaml @@ -0,0 +1,8 @@ +getting-started: + order: 1 +systemd-integration: + order: 2 +kubernetes-integration: + order: 3 +docker-integration: + order: 4 \ No newline at end of file diff --git a/guides/systemd-integration/readme.md b/guides/systemd-integration/readme.md new file mode 100644 index 0000000..7624340 --- /dev/null +++ b/guides/systemd-integration/readme.md @@ -0,0 +1,22 @@ +# Systemd Integration + +This guide explains how to use `async-container` with systemd to manage your application as a service. + +## Service File + +Install a template file into `/etc/systemd/system/`: + +``` +# my-daemon.service +[Unit] +Description=My Daemon + +[Service] +Type=notify +ExecStart=bundle exec my-daemon + +[Install] +WantedBy=multi-user.target +``` + +Ensure `Type=notify` is set, so that the service can notify systemd when it is ready. diff --git a/lib/async/container/notify/log.rb b/lib/async/container/notify/log.rb index d66375b..f97120c 100644 --- a/lib/async/container/notify/log.rb +++ b/lib/async/container/notify/log.rb @@ -14,9 +14,13 @@ class Log < Client # The name of the environment variable which contains the path to the notification socket. NOTIFY_LOG = "NOTIFY_LOG" + def self.path(environment = ENV) + environment[NOTIFY_LOG] + end + # Open a notification client attached to the current {NOTIFY_LOG} if possible. def self.open!(environment = ENV) - if path = environment.delete(NOTIFY_LOG) + if path = self.path(environment) self.new(path) end end diff --git a/releases.md b/releases.md index ff8995e..7f9b35b 100644 --- a/releases.md +++ b/releases.md @@ -1,5 +1,9 @@ # Releases +## Unreleased + + - Introduce `async:container:notify:log:ready?` task for detecting process readiness. + ## v0.24.0 - Add support for health check failure metrics. From f33e1a65570a6eeec72f4fb43851080fbbcdde15 Mon Sep 17 00:00:00 2001 From: Samuel Williams Date: Mon, 4 Aug 2025 10:05:53 +1200 Subject: [PATCH 2/6] Add tests. --- test/async/container/notify/log.rb | 33 +++++++++++++++++++++++++++++- 1 file changed, 32 insertions(+), 1 deletion(-) diff --git a/test/async/container/notify/log.rb b/test/async/container/notify/log.rb index 7bb7bad..7b8062a 100644 --- a/test/async/container/notify/log.rb +++ b/test/async/container/notify/log.rb @@ -7,11 +7,17 @@ require "async/container/controllers" require "tmpdir" +require "bake" -describe Async::Container::Notify::Pipe do +describe Async::Container::Notify::Log do let(:notify_script) {Async::Container::Controllers.path_for("notify")} let(:notify_log) {File.expand_path("notify-#{::Process.pid}-#{SecureRandom.hex(8)}.log", Dir.tmpdir)} + let(:notify) {Async::Container::Notify::Log.open!({"NOTIFY_LOG" => notify_log})} + after do + File.unlink(notify_log) rescue nil + end + it "receives notification of child status" do system({"NOTIFY_LOG" => notify_log}, "bundle", "exec", notify_script) @@ -22,4 +28,29 @@ "size" => be > 0, ) end + + with "async:container:notify:log:ready?" do + let(:context) {Bake::Context.load} + let(:recipe) {context.lookup("async:container:notify:log:ready?")} + + it "fails if the log file does not exist" do + expect do + recipe.call(path: "nonexistant.log") + end.to raise_exception(RuntimeError, message: be =~ /log file does not exist/i) + end + + it "succeeds if the log file exists and is ready" do + notify.ready! + + expect(recipe.call(path: notify_log)).to be == true + end + + it "fails if the log file exists but is not ready" do + notify.status!("Loading...") + + expect do + expect(recipe.call(path: notify_log)) + end.to raise_exception(RuntimeError, message: be =~ /service is not ready/i) + end + end end From 8b535a9b852f379a114c46ea527cb4bf6ac28574 Mon Sep 17 00:00:00 2001 From: Samuel Williams Date: Mon, 4 Aug 2025 10:07:33 +1200 Subject: [PATCH 3/6] Documentation. --- bake/async/container/notify/log.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/bake/async/container/notify/log.rb b/bake/async/container/notify/log.rb index 6b4af28..14e110f 100644 --- a/bake/async/container/notify/log.rb +++ b/bake/async/container/notify/log.rb @@ -6,6 +6,7 @@ def initialize(...) end # Check if the log file exists and the service is ready. +# @parameter path [String] The path to the notification log file, uses the `NOTIFY_LOG` environment variable if not provided. def ready?(path: Async::Container::Notify::Log.path) if File.exist?(path) File.foreach(path) do |line| From 66381f62955a0d833a1b571790c899a0c161fa66 Mon Sep 17 00:00:00 2001 From: Samuel Williams Date: Mon, 4 Aug 2025 10:15:57 +1200 Subject: [PATCH 4/6] Add missing documentation. --- lib/async/container/forked.rb | 1 + lib/async/container/notify/log.rb | 3 +++ 2 files changed, 4 insertions(+) diff --git a/lib/async/container/forked.rb b/lib/async/container/forked.rb index eb2e87c..c94f8f1 100644 --- a/lib/async/container/forked.rb +++ b/lib/async/container/forked.rb @@ -189,6 +189,7 @@ def inspect "\#<#{self.class} name=#{@name.inspect} status=#{@status.inspect} pid=#{@pid.inspect}>" end + # @returns [String] A string representation of the process. alias to_s inspect # Invoke {#terminate!} and then {#wait} for the child process to exit. diff --git a/lib/async/container/notify/log.rb b/lib/async/container/notify/log.rb index f97120c..ed89296 100644 --- a/lib/async/container/notify/log.rb +++ b/lib/async/container/notify/log.rb @@ -14,11 +14,14 @@ class Log < Client # The name of the environment variable which contains the path to the notification socket. NOTIFY_LOG = "NOTIFY_LOG" + # @returns [String] The path to the notification log file. + # @parameter environment [Hash] The environment variables, defaults to `ENV`. def self.path(environment = ENV) environment[NOTIFY_LOG] end # Open a notification client attached to the current {NOTIFY_LOG} if possible. + # @parameter environment [Hash] The environment variables, defaults to `ENV`. def self.open!(environment = ENV) if path = self.path(environment) self.new(path) From 0b673002c643855b6681032e6b705a52e9c92ceb Mon Sep 17 00:00:00 2001 From: Samuel Williams Date: Mon, 4 Aug 2025 10:17:18 +1200 Subject: [PATCH 5/6] RuboCop. --- bake/async/container/notify/log.rb | 1 + lib/async/container/notify/log.rb | 2 +- test/async/container/notify/log.rb | 12 ++++++------ 3 files changed, 8 insertions(+), 7 deletions(-) diff --git a/bake/async/container/notify/log.rb b/bake/async/container/notify/log.rb index 14e110f..dcc7db2 100644 --- a/bake/async/container/notify/log.rb +++ b/bake/async/container/notify/log.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true def initialize(...) super diff --git a/lib/async/container/notify/log.rb b/lib/async/container/notify/log.rb index ed89296..45cbf48 100644 --- a/lib/async/container/notify/log.rb +++ b/lib/async/container/notify/log.rb @@ -19,7 +19,7 @@ class Log < Client def self.path(environment = ENV) environment[NOTIFY_LOG] end - + # Open a notification client attached to the current {NOTIFY_LOG} if possible. # @parameter environment [Hash] The environment variables, defaults to `ENV`. def self.open!(environment = ENV) diff --git a/test/async/container/notify/log.rb b/test/async/container/notify/log.rb index 7b8062a..79bcfec 100644 --- a/test/async/container/notify/log.rb +++ b/test/async/container/notify/log.rb @@ -17,7 +17,7 @@ after do File.unlink(notify_log) rescue nil end - + it "receives notification of child status" do system({"NOTIFY_LOG" => notify_log}, "bundle", "exec", notify_script) @@ -28,23 +28,23 @@ "size" => be > 0, ) end - + with "async:container:notify:log:ready?" do let(:context) {Bake::Context.load} let(:recipe) {context.lookup("async:container:notify:log:ready?")} - + it "fails if the log file does not exist" do expect do recipe.call(path: "nonexistant.log") end.to raise_exception(RuntimeError, message: be =~ /log file does not exist/i) end - + it "succeeds if the log file exists and is ready" do notify.ready! - + expect(recipe.call(path: notify_log)).to be == true end - + it "fails if the log file exists but is not ready" do notify.status!("Loading...") From d75fa0a7c3314111cc00e48310d9a1b93064dbcb Mon Sep 17 00:00:00 2001 From: Samuel Williams Date: Mon, 4 Aug 2025 10:17:45 +1200 Subject: [PATCH 6/6] Copyrights. --- bake/async/container/notify/log.rb | 3 +++ lib/async/container/notify/pipe.rb | 2 +- test/async/container/hybrid.rb | 2 +- 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/bake/async/container/notify/log.rb b/bake/async/container/notify/log.rb index dcc7db2..2b4c5c0 100644 --- a/bake/async/container/notify/log.rb +++ b/bake/async/container/notify/log.rb @@ -1,5 +1,8 @@ # frozen_string_literal: true +# Released under the MIT License. +# Copyright, 2025, by Samuel Williams. + def initialize(...) super diff --git a/lib/async/container/notify/pipe.rb b/lib/async/container/notify/pipe.rb index c9b6572..7c955bb 100644 --- a/lib/async/container/notify/pipe.rb +++ b/lib/async/container/notify/pipe.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true # Released under the MIT License. -# Copyright, 2020-2024, by Samuel Williams. +# Copyright, 2020-2025, by Samuel Williams. # Copyright, 2020, by Juan Antonio Martín Lucas. require_relative "client" diff --git a/test/async/container/hybrid.rb b/test/async/container/hybrid.rb index 2412162..7e8ec29 100644 --- a/test/async/container/hybrid.rb +++ b/test/async/container/hybrid.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true # Released under the MIT License. -# Copyright, 2019-2024, by Samuel Williams. +# Copyright, 2019-2025, by Samuel Williams. require "async/container/hybrid" require "async/container/best"