Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions bake/async/container/notify/log.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# frozen_string_literal: true

# Released under the MIT License.
# Copyright, 2025, by Samuel Williams.

def initialize(...)
super

require "async/container/notify/log"
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|
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
22 changes: 0 additions & 22 deletions guides/getting-started/readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
```
39 changes: 39 additions & 0 deletions guides/kubernetes-integration/readme.md
Original file line number Diff line number Diff line change
@@ -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
```
8 changes: 8 additions & 0 deletions guides/links.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
getting-started:
order: 1
systemd-integration:
order: 2
kubernetes-integration:
order: 3
docker-integration:
order: 4
22 changes: 22 additions & 0 deletions guides/systemd-integration/readme.md
Original file line number Diff line number Diff line change
@@ -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.
1 change: 1 addition & 0 deletions lib/async/container/forked.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
9 changes: 8 additions & 1 deletion lib/async/container/notify/log.rb
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,16 @@ 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 = environment.delete(NOTIFY_LOG)
if path = self.path(environment)
self.new(path)
end
end
Expand Down
2 changes: 1 addition & 1 deletion lib/async/container/notify/pipe.rb
Original file line number Diff line number Diff line change
@@ -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"
Expand Down
4 changes: 4 additions & 0 deletions releases.md
Original file line number Diff line number Diff line change
@@ -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.
Expand Down
2 changes: 1 addition & 1 deletion test/async/container/hybrid.rb
Original file line number Diff line number Diff line change
@@ -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"
Expand Down
33 changes: 32 additions & 1 deletion test/async/container/notify/log.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,16 @@
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)
Expand All @@ -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
Loading