Skip to content

Commit 638b6c7

Browse files
committed
Add IPC example, and tidy up usage of Console.
1 parent d1e7f5b commit 638b6c7

File tree

4 files changed

+184
-3
lines changed

4 files changed

+184
-3
lines changed

examples/ipc/readme.md

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
# IPC Example
2+
3+
This example demonstrates Inter-Process Communication (IPC) using Unix domain sockets with two async services:
4+
5+
- **IPC Server**: Listens on a Unix domain socket and responds with "Hello World" to each connection
6+
- **IPC Client**: Periodically connects to the server and prints the received message
7+
8+
Both services use `ContainerService` as the base class, which provides built-in health checking, process title formatting, and container management.
9+
10+
## Configuration
11+
12+
The example uses a shared environment module (`IPCEnvironment`) that includes `ContainerEnvironment` for container configuration:
13+
14+
```ruby
15+
module IPCEnvironment
16+
include Async::Service::ContainerEnvironment
17+
18+
def ipc_socket_path
19+
File.expand_path("service.ipc", Dir.pwd)
20+
end
21+
22+
def ipc_connection_timeout
23+
5.0
24+
end
25+
26+
def count
27+
1 # Run single instance of each service
28+
end
29+
end
30+
```
31+
32+
### Customizing the Socket Path
33+
34+
You can override the socket path by modifying the `IPCEnvironment` module:
35+
36+
```ruby
37+
module IPCEnvironment
38+
def ipc_socket_path
39+
"/tmp/my_custom_ipc.sock"
40+
end
41+
end
42+
```
43+
44+
## Usage
45+
46+
```bash
47+
# Run both services together:
48+
bundle exec service.rb
49+
```
50+
51+
## Expected Output
52+
53+
The server will start first and begin listening:
54+
55+
```
56+
IPC Server listening on /Users/your-username/your-project/service.ipc
57+
```
58+
59+
The client will then periodically connect:
60+
61+
```
62+
Connected to server
63+
Received from server: Hello World
64+
Connection closed
65+
```
66+
67+
This process repeats every 5 seconds (2 second delay + 3 second wait), demonstrating persistent IPC communication between services.

examples/ipc/service.rb

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
#!/usr/bin/env async-service
2+
# frozen_string_literal: true
3+
4+
# Released under the MIT License.
5+
# Copyright, 2025, by Samuel Williams.
6+
7+
require "socket"
8+
require "async"
9+
require "async/service/container_service"
10+
require "async/service/container_environment"
11+
12+
# Server service that listens on a Unix domain socket and responds with "Hello World"
13+
class IPCServer < Async::Service::ContainerService
14+
def run(instance, evaluator)
15+
socket_path = evaluator.ipc_socket_path
16+
17+
# Clean up any existing socket
18+
File.unlink(socket_path) if File.exist?(socket_path)
19+
20+
# Create Unix domain socket server
21+
server = UNIXServer.new(socket_path)
22+
23+
Console.info(self) {"IPC Server listening on #{socket_path}"}
24+
instance.ready!
25+
26+
begin
27+
while true
28+
# Accept incoming connections
29+
client = server.accept
30+
Console.info(self) {"Client connected"}
31+
32+
# Send greeting
33+
client.write("Hello World\n")
34+
client.close
35+
36+
Console.info(self) {"Sent greeting and closed connection"}
37+
end
38+
rescue => error
39+
Console.error(self, error)
40+
ensure
41+
server&.close
42+
File.unlink(socket_path) if File.exist?(socket_path)
43+
end
44+
45+
return server
46+
end
47+
end
48+
49+
# Client service that periodically connects to the server
50+
class IPCClient < Async::Service::ContainerService
51+
def run(instance, evaluator)
52+
socket_path = evaluator.ipc_socket_path
53+
timeout = evaluator.ipc_connection_timeout
54+
55+
Console.info(self) {"IPC Client starting - will connect to #{socket_path}"}
56+
instance.ready!
57+
58+
Async do |task|
59+
while true
60+
begin
61+
# Wait a bit before first connection attempt
62+
task.sleep(2)
63+
64+
# Connect to server
65+
client = UNIXSocket.new(socket_path)
66+
Console.info(self) {"Connected to server"}
67+
68+
# Read response
69+
response = client.readline.chomp
70+
puts "Received from server: #{response}"
71+
72+
client.close
73+
Console.info(self) {"Connection closed"}
74+
75+
# Wait before next connection
76+
task.sleep(3)
77+
78+
rescue Errno::ENOENT
79+
Console.warn(self) {"Server socket not found at #{socket_path}, retrying..."}
80+
task.sleep(2)
81+
rescue => error
82+
Console.error(self, error)
83+
task.sleep(2)
84+
end
85+
end
86+
end
87+
end
88+
end
89+
90+
module IPCEnvironment
91+
include Async::Service::ContainerEnvironment
92+
93+
def ipc_socket_path
94+
File.expand_path("service.ipc", Dir.pwd)
95+
end
96+
97+
def ipc_connection_timeout
98+
5.0
99+
end
100+
101+
# Override to use only 1 instance for both services.
102+
def count
103+
1
104+
end
105+
end
106+
107+
# Define both services using the shared IPC environment:
108+
service "ipc-server" do
109+
service_class IPCServer
110+
include IPCEnvironment
111+
end
112+
113+
service "ipc-client" do
114+
service_class IPCClient
115+
include IPCEnvironment
116+
end

fixtures/async/service/sleep_service.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ def setup(container)
1313

1414
container.run(count: 1, restart: true) do |instance|
1515
# Use log level:
16-
Console.logger.level = @environment.evaluator.log_level
16+
Console.level = @environment.evaluator.log_level
1717

1818
if container.statistics.failed?
1919
Console.debug(self, "Child process restarted #{container.statistics.restarts} times.")

lib/async/service/loader.rb

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,8 +38,6 @@ def self.load_file(configuration, path)
3838
loader.instance_eval(File.read(path), path)
3939
end
4040

41-
# Load a file relative to the loader's root directory.
42-
# @parameter path [String] The path to the file to load.
4341
def load_file(path)
4442
Loader.load_file(@configuration, File.expand_path(path, @root))
4543
end

0 commit comments

Comments
 (0)