Skip to content

Commit 86475e4

Browse files
committed
Working simple example.
1 parent c43c17c commit 86475e4

File tree

12 files changed

+269
-44
lines changed

12 files changed

+269
-44
lines changed

async-container-supervisor.gemspec

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ Gem::Specification.new do |spec|
2525
spec.required_ruby_version = ">= 3.1"
2626

2727
spec.add_dependency "async-container"
28+
spec.add_dependency "async-service"
2829
spec.add_dependency "io-stream"
2930
spec.add_dependency "memory-leak", "~> 0.2"
3031
end

example/simple/simple.rb

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
#!/usr/bin/env async-service
2+
3+
require "async/container/supervisor"
4+
5+
class SleepService < Async::Service::Generic
6+
def setup(container)
7+
super
8+
9+
container.run(count: 1, restart: true, health_check_timeout: 2) do |instance|
10+
Async do
11+
client = Async::Container::Supervisor::Client.new(instance, @evaluator.supervisor_endpoint)
12+
client.run
13+
14+
instance.ready!
15+
16+
chunks = []
17+
while true
18+
Console.info(self, "Allocating memory...")
19+
# Allocate 10MB of memory every second:
20+
chunks << " " * 1024 * 1024
21+
sleep 0.1
22+
instance.ready!
23+
end
24+
ensure
25+
Console.info(self, "Exiting...")
26+
end
27+
end
28+
end
29+
end
30+
31+
service "sleep" do
32+
service_class SleepService
33+
34+
supervisor_endpoint {Async::Container::Supervisor.endpoint}
35+
end
36+
37+
service "supervisor" do
38+
include Async::Container::Supervisor::Environment
39+
40+
monitors do
41+
[Async::Container::Supervisor::Monitor::MemoryMonitor.new(interval: 1, limit: 1024 * 1024 * 100)]
42+
end
43+
end

lib/async/container/supervisor.rb

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,7 @@
77

88
require_relative "supervisor/server"
99
require_relative "supervisor/client"
10+
require_relative "supervisor/monitor"
11+
12+
require_relative "supervisor/environment"
13+
require_relative "supervisor/service"

lib/async/container/supervisor/client.rb

Lines changed: 23 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -44,16 +44,31 @@ def close
4444
end
4545
end
4646

47+
def do_memory_dump(wrapper, message)
48+
Console.info(self, "Memory dump:", message)
49+
path = message[:path]
50+
51+
File.open(path, "w") do |file|
52+
ObjectSpace.dump_all(output: file)
53+
end
54+
end
55+
4756
def run
48-
loop do
49-
connect do |wrapper|
50-
wrapper.run(self)
57+
Async do |task|
58+
loop do
59+
Console.info(self, "Connecting to supervisor...")
60+
connect do |wrapper|
61+
Console.info(self, "Connected to supervisor.")
62+
wrapper.run(self)
63+
end
64+
rescue => error
65+
Console.error(self, "Unexpected error while running client!", exception: error)
66+
67+
# Retry after a small delay:
68+
sleep(rand)
5169
end
52-
rescue => error
53-
Console.error(self, "Unexpected error while running client!", exception: error)
54-
55-
# Retry after a small delay:
56-
sleep(rand)
70+
ensure
71+
task.stop
5772
end
5873
end
5974
end
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
# frozen_string_literal: true
2+
3+
# Released under the MIT License.
4+
# Copyright, 2019-2024, by Samuel Williams.
5+
6+
require "async/service/environment"
7+
8+
module Async
9+
module Container
10+
module Supervisor
11+
module Environment
12+
# The service class to use for the supervisor.
13+
# @returns [Class]
14+
def service_class
15+
Supervisor::Service
16+
end
17+
18+
# The name of the supervisor
19+
# @returns [String]
20+
def name
21+
"supervisor"
22+
end
23+
24+
# The IPC path to use for communication with the supervisor.
25+
# @returns [String]
26+
def ipc_path
27+
::File.expand_path("supervisor.ipc", root)
28+
end
29+
30+
# The endpoint the supervisor will bind to.
31+
# @returns [::IO::Endpoint::Generic]
32+
def endpoint
33+
::IO::Endpoint.unix(ipc_path)
34+
end
35+
36+
# Options to use when creating the container.
37+
def container_options
38+
{restart: true, count: 1, health_check_timeout: 30}
39+
end
40+
41+
def monitors
42+
[]
43+
end
44+
45+
def make_server(endpoint)
46+
Server.new(endpoint, monitors: self.monitors)
47+
end
48+
end
49+
end
50+
end
51+
end
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
require_relative "monitor/periodic_monitor"
2+
require_relative "monitor/memory_monitor"

lib/async/container/supervisor/monitor/memory_monitor.rb

Lines changed: 36 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -4,46 +4,62 @@
44
# Copyright, 2025, by Samuel Williams.
55

66
require_relative "periodic_monitor"
7-
require "memory/leaks/cluster"
7+
require "memory/leak/cluster"
88

99
module Async
1010
module Container
1111
module Supervisor
1212
module Monitor
13-
class MemoryMonitor < PeriodicMonitor
14-
def initialize(cluster, **options)
15-
super(**options)
16-
@cluster = cluster
17-
@processes = Hash.new
13+
class MemoryMonitor
14+
def initialize(interval: 10, limit: nil)
15+
@interval = interval
16+
@cluster = Memory::Leak::Cluster.new(limit: limit)
17+
@processes = Hash.new(0)
1818
end
1919

20-
def register(wrapper, message)
21-
if process_id = message[:process_id]
20+
def register(wrapper, registration)
21+
return unless instance = registration[:instance]
22+
23+
Console.info(self, "Registering process:", instance)
24+
if process_id = instance[:process_id]
2225
if @processes.key?(process_id)
26+
Console.info(self, "Incrementing process:", process_id: process_id)
2327
@processes[process_id] += 1
2428
else
29+
Console.info(self, "Registering process:", process_id: process_id)
2530
@cluster.add(process_id)
2631
@processes[process_id] = 1
2732
end
2833
end
2934
end
3035

31-
def remove()
32-
# if
33-
@cluster.remove(worker.process_id)
34-
end
35-
36-
def call
37-
@cluster.check! do |pid, monitor|
38-
kill(pid)
36+
def remove(wrapper, registration)
37+
return unless instance = registration[:instance]
38+
39+
if process_id = instance[:process_id]
40+
if @processes.key?(process_id)
41+
@processes[process_id] -= 1
42+
43+
if @processes[process_id] == 0
44+
Console.info(self, "Deregistering process:", process_id: process_id)
45+
@cluster.remove(process_id)
46+
@processes.delete(process_id)
47+
end
48+
end
3949
end
4050
end
4151

4252
def run
43-
while true
44-
self.call
45-
46-
sleep(@interval)
53+
Async do
54+
while true
55+
Console.info(self, "Checking for memory leaks...", processes: @processes)
56+
@cluster.check! do |process_id, monitor|
57+
Console.error(self, "Memory leak detected in process:", process_id: process_id, monitor: monitor)
58+
::Process.kill(:INT, process_id)
59+
end
60+
61+
sleep(@interval)
62+
end
4763
end
4864
end
4965
end

lib/async/container/supervisor/monitor/periodic_monitor.rb

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,19 +8,24 @@ module Container
88
module Supervisor
99
module Monitor
1010
class PeriodicMonitor
11-
def initialize(interval: 1)
11+
def initialize(interval: 1, &block)
1212
@interval = interval
13+
@block = block
1314
end
1415

15-
def call
16-
raise NotImplementedError
16+
def register(wrapper, state)
17+
end
18+
19+
def remove(wrapper, state)
1720
end
1821

1922
def run
20-
while true
21-
self.call
22-
23-
sleep(@interval)
23+
Async do
24+
while true
25+
@block.call
26+
27+
sleep(@interval)
28+
end
2429
end
2530
end
2631
end

lib/async/container/supervisor/server.rb

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ def initialize(endpoint = Supervisor.endpoint, monitors: [])
2626
attr :registered
2727

2828
def do_register(wrapper, state)
29+
Console.info(self, "Registering process:", state)
2930
@registered[wrapper] = state
3031

3132
@monitors.each do |monitor|
@@ -42,13 +43,25 @@ def remove(wrapper)
4243
end
4344

4445
def run
45-
@endpoint.accept do |peer|
46-
stream = IO::Stream(peer)
47-
wrapper = Wrapper.new(stream)
48-
wrapper.run(self)
46+
Async do |task|
47+
Console.info(self, "Starting monitors...")
48+
@monitors.each(&:run)
49+
50+
Console.info(self, "Accepting connections...")
51+
@endpoint.accept do |peer|
52+
Console.info(self, "Accepted connection from peer:", peer: peer)
53+
stream = IO::Stream(peer)
54+
wrapper = Wrapper.new(stream)
55+
wrapper.run(self)
56+
ensure
57+
wrapper.close
58+
remove(wrapper)
59+
end
60+
61+
task.children&.each(&:wait)
4962
ensure
50-
wrapper.close
51-
remove(wrapper)
63+
Console.info(self, "Stopping...")
64+
task.stop
5265
end
5366
end
5467
end
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
# frozen_string_literal: true
2+
3+
# Released under the MIT License.
4+
# Copyright, 2025, by Samuel Williams.
5+
6+
require "async"
7+
require "io/endpoint/bound_endpoint"
8+
9+
module Async
10+
module Container
11+
module Supervisor
12+
class Service < Async::Service::Generic
13+
# Initialize the supervisor using the given environment.
14+
# @parameter environment [Build::Environment]
15+
def initialize(...)
16+
super
17+
18+
@bound_endpoint = nil
19+
end
20+
21+
# The endpoint which the supervisor will bind to.
22+
# Typically a unix pipe in the same directory as the host.
23+
def endpoint
24+
@evaluator.endpoint
25+
end
26+
27+
# Bind the supervisor to the specified endpoint.
28+
def start
29+
@bound_endpoint = self.endpoint.bound
30+
31+
super
32+
end
33+
34+
def name
35+
@evaluator.name
36+
end
37+
38+
def setup(container)
39+
container_options = @evaluator.container_options
40+
health_check_timeout = container_options[:health_check_timeout]
41+
42+
container.run(name: self.name, **container_options) do |instance|
43+
evaluator = @environment.evaluator
44+
45+
Async do
46+
server = evaluator.make_server(@bound_endpoint)
47+
server.run
48+
49+
instance.ready!
50+
51+
if health_check_timeout
52+
Async(transient: true) do
53+
while true
54+
sleep(health_check_timeout / 2)
55+
instance.ready!
56+
end
57+
end
58+
end
59+
end
60+
end
61+
62+
super
63+
end
64+
65+
# Release the bound endpoint.
66+
def stop
67+
@bound_endpoint&.close
68+
@bound_endpoint = nil
69+
70+
super
71+
end
72+
end
73+
end
74+
end
75+
end

lib/async/container/supervisor/wrapper.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ def write(**message)
1919
end
2020

2121
def read
22-
if line = @stream.gets
22+
if line = @stream&.gets
2323
JSON.parse(line, symbolize_names: true)
2424
end
2525
end

0 commit comments

Comments
 (0)