async-container 0.16.6 → 0.16.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/lib/async/container.rb +0 -1
- data/lib/async/container/best.rb +9 -3
- data/lib/async/container/channel.rb +13 -0
- data/lib/async/container/controller.rb +46 -25
- data/lib/async/container/error.rb +21 -0
- data/lib/async/container/forked.rb +5 -1
- data/lib/async/container/generic.rb +58 -17
- data/lib/async/container/group.rb +19 -4
- data/lib/async/container/hybrid.rb +5 -0
- data/lib/async/container/keyed.rb +12 -0
- data/lib/async/container/notify.rb +4 -5
- data/lib/async/container/notify/client.rb +16 -1
- data/lib/async/container/notify/console.rb +8 -21
- data/lib/async/container/notify/pipe.rb +8 -6
- data/lib/async/container/notify/server.rb +0 -1
- data/lib/async/container/notify/socket.rb +14 -1
- data/lib/async/container/process.rb +28 -2
- data/lib/async/container/statistics.rb +16 -0
- data/lib/async/container/thread.rb +41 -6
- data/lib/async/container/threaded.rb +5 -1
- data/lib/async/container/version.rb +1 -1
- metadata +5 -33
@@ -21,7 +21,6 @@
|
|
21
21
|
# THE SOFTWARE.
|
22
22
|
|
23
23
|
require 'fiber'
|
24
|
-
|
25
24
|
require 'async/clock'
|
26
25
|
|
27
26
|
require_relative 'error'
|
@@ -30,6 +29,7 @@ module Async
|
|
30
29
|
module Container
|
31
30
|
# Manages a group of running processes.
|
32
31
|
class Group
|
32
|
+
# Initialize an empty group.
|
33
33
|
def initialize
|
34
34
|
@running = {}
|
35
35
|
|
@@ -40,19 +40,25 @@ module Async
|
|
40
40
|
# @attribute [Hash<IO, Fiber>] the running tasks, indexed by IO.
|
41
41
|
attr :running
|
42
42
|
|
43
|
+
# Whether the group contains any running processes.
|
44
|
+
# @returns [Boolean]
|
43
45
|
def running?
|
44
46
|
@running.any?
|
45
47
|
end
|
46
48
|
|
49
|
+
# Whether the group contains any running processes.
|
50
|
+
# @returns [Boolean]
|
47
51
|
def any?
|
48
52
|
@running.any?
|
49
53
|
end
|
50
54
|
|
55
|
+
# Whether the group is empty.
|
56
|
+
# @returns [Boolean]
|
51
57
|
def empty?
|
52
58
|
@running.empty?
|
53
59
|
end
|
54
60
|
|
55
|
-
#
|
61
|
+
# Sleep for at most the specified duration until some state change occurs.
|
56
62
|
def sleep(duration)
|
57
63
|
self.resume
|
58
64
|
self.suspend
|
@@ -60,6 +66,7 @@ module Async
|
|
60
66
|
self.wait_for_children(duration)
|
61
67
|
end
|
62
68
|
|
69
|
+
# Begin any outstanding queued processes and wait for them indefinitely.
|
63
70
|
def wait
|
64
71
|
self.resume
|
65
72
|
|
@@ -68,20 +75,26 @@ module Async
|
|
68
75
|
end
|
69
76
|
end
|
70
77
|
|
78
|
+
# Interrupt all running processes.
|
79
|
+
# This resumes the controlling fiber with an instance of {Interrupt}.
|
71
80
|
def interrupt
|
72
|
-
|
81
|
+
Console.logger.debug(self, "Sending interrupt to #{@running.size} running processes...")
|
73
82
|
@running.each_value do |fiber|
|
74
83
|
fiber.resume(Interrupt)
|
75
84
|
end
|
76
85
|
end
|
77
86
|
|
87
|
+
# Terminate all running processes.
|
88
|
+
# This resumes the controlling fiber with an instance of {Terminate}.
|
78
89
|
def terminate
|
79
|
-
|
90
|
+
Console.logger.debug(self, "Sending terminate to #{@running.size} running processes...")
|
80
91
|
@running.each_value do |fiber|
|
81
92
|
fiber.resume(Terminate)
|
82
93
|
end
|
83
94
|
end
|
84
95
|
|
96
|
+
# Stop all child processes using {#terminate}.
|
97
|
+
# @parameter timeout [Boolean | Numeric | Nil] If specified, invoke a graceful shutdown using {#interrupt} first.
|
85
98
|
def stop(timeout = 1)
|
86
99
|
# Use a default timeout if not specified:
|
87
100
|
timeout = 1 if timeout == true
|
@@ -111,6 +124,7 @@ module Async
|
|
111
124
|
self.wait
|
112
125
|
end
|
113
126
|
|
127
|
+
# Wait for a message in the specified {Channel}.
|
114
128
|
def wait_for(channel)
|
115
129
|
io = channel.in
|
116
130
|
|
@@ -137,6 +151,7 @@ module Async
|
|
137
151
|
|
138
152
|
def wait_for_children(duration = nil)
|
139
153
|
if !@running.empty?
|
154
|
+
# Maybe consider using a proper event loop here:
|
140
155
|
readable, _, _ = ::IO.select(@running.keys, nil, nil, duration)
|
141
156
|
|
142
157
|
readable&.each do |io|
|
@@ -25,7 +25,12 @@ require_relative 'threaded'
|
|
25
25
|
|
26
26
|
module Async
|
27
27
|
module Container
|
28
|
+
# Provides a hybrid multi-process multi-thread container.
|
28
29
|
class Hybrid < Forked
|
30
|
+
# Run multiple instances of the same block in the container.
|
31
|
+
# @parameter count [Integer] The number of instances to start.
|
32
|
+
# @parameter forks [Integer] The number of processes to fork.
|
33
|
+
# @parameter threads [Integer] the number of threads to start.
|
29
34
|
def run(count: nil, forks: nil, threads: nil, **options, &block)
|
30
35
|
processor_count = Container.processor_count
|
31
36
|
count ||= processor_count ** 2
|
@@ -22,6 +22,8 @@
|
|
22
22
|
|
23
23
|
module Async
|
24
24
|
module Container
|
25
|
+
# Tracks a key/value pair such that unmarked keys can be identified and cleaned up.
|
26
|
+
# This helps implement persistent processes that start up child processes per directory or configuration file. If those directories and/or configuration files are removed, the child process can then be cleaned up automatically, because those key/value pairs will not be marked when reloading the container.
|
25
27
|
class Keyed
|
26
28
|
def initialize(key, value)
|
27
29
|
@key = key
|
@@ -29,21 +31,31 @@ module Async
|
|
29
31
|
@marked = true
|
30
32
|
end
|
31
33
|
|
34
|
+
# The key. Normally a symbol or a file-system path.
|
35
|
+
# @attribute [Object]
|
32
36
|
attr :key
|
37
|
+
|
38
|
+
# The value. Normally a child instance of some sort.
|
39
|
+
# @attribute [Object]
|
33
40
|
attr :value
|
34
41
|
|
42
|
+
# Has the instance been marked?
|
43
|
+
# @returns [Boolean]
|
35
44
|
def marked?
|
36
45
|
@marked
|
37
46
|
end
|
38
47
|
|
48
|
+
# Mark the instance. This will indiciate that the value is still in use/active.
|
39
49
|
def mark!
|
40
50
|
@marked = true
|
41
51
|
end
|
42
52
|
|
53
|
+
# Clear the instance. This is normally done before reloading a container.
|
43
54
|
def clear!
|
44
55
|
@marked = false
|
45
56
|
end
|
46
57
|
|
58
|
+
# Stop the instance if it was not marked.
|
47
59
|
def stop?
|
48
60
|
unless @marked
|
49
61
|
@value.stop
|
@@ -1,6 +1,5 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
#
|
4
3
|
# Copyright, 2020, by Samuel G. D. Williams. <http://www.codeotaku.com>
|
5
4
|
#
|
6
5
|
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
@@ -28,12 +27,12 @@ require_relative 'notify/console'
|
|
28
27
|
module Async
|
29
28
|
module Container
|
30
29
|
module Notify
|
31
|
-
|
32
|
-
@@client = nil
|
30
|
+
@client = nil
|
33
31
|
|
32
|
+
# Select the best available notification client.
|
33
|
+
# We cache the client on a per-process basis. Because that's the relevant scope for process readiness protocols.
|
34
34
|
def self.open!
|
35
|
-
|
36
|
-
@@client ||= (
|
35
|
+
@client ||= (
|
37
36
|
Pipe.open! ||
|
38
37
|
Socket.open! ||
|
39
38
|
Console.open!
|
@@ -1,6 +1,5 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
#
|
4
3
|
# Copyright, 2020, by Samuel G. D. Williams. <http://www.codeotaku.com>
|
5
4
|
#
|
6
5
|
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
@@ -23,12 +22,17 @@
|
|
23
22
|
|
24
23
|
module Async
|
25
24
|
module Container
|
25
|
+
# Handles the details of several process readiness protocols.
|
26
26
|
module Notify
|
27
27
|
class Client
|
28
|
+
# Notify the parent controller that the child has become ready, with a brief status message.
|
29
|
+
# @parameters message [Hash] Additional details to send with the message.
|
28
30
|
def ready!(**message)
|
29
31
|
send(ready: true, **message)
|
30
32
|
end
|
31
33
|
|
34
|
+
# Notify the parent controller that the child is reloading.
|
35
|
+
# @parameters message [Hash] Additional details to send with the message.
|
32
36
|
def reloading!(**message)
|
33
37
|
message[:ready] = false
|
34
38
|
message[:reloading] = true
|
@@ -37,6 +41,8 @@ module Async
|
|
37
41
|
send(**message)
|
38
42
|
end
|
39
43
|
|
44
|
+
# Notify the parent controller that the child is restarting.
|
45
|
+
# @parameters message [Hash] Additional details to send with the message.
|
40
46
|
def restarting!(**message)
|
41
47
|
message[:ready] = false
|
42
48
|
message[:reloading] = true
|
@@ -45,14 +51,23 @@ module Async
|
|
45
51
|
send(**message)
|
46
52
|
end
|
47
53
|
|
54
|
+
# Notify the parent controller that the child is stopping.
|
55
|
+
# @parameters message [Hash] Additional details to send with the message.
|
48
56
|
def stopping!(**message)
|
49
57
|
message[:stopping] = true
|
58
|
+
|
59
|
+
send(**message)
|
50
60
|
end
|
51
61
|
|
62
|
+
# Notify the parent controller of a status change.
|
63
|
+
# @parameters text [String] The details of the status change.
|
52
64
|
def status!(text)
|
53
65
|
send(status: text)
|
54
66
|
end
|
55
67
|
|
68
|
+
# Notify the parent controller of an error condition.
|
69
|
+
# @parameters text [String] The details of the error condition.
|
70
|
+
# @parameters message [Hash] Additional details to send with the message.
|
56
71
|
def error!(text, **message)
|
57
72
|
send(status: text, **message)
|
58
73
|
end
|
@@ -1,6 +1,5 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
#
|
4
3
|
# Copyright, 2020, by Samuel G. D. Williams. <http://www.codeotaku.com>
|
5
4
|
#
|
6
5
|
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
@@ -28,39 +27,27 @@ require 'console/logger'
|
|
28
27
|
module Async
|
29
28
|
module Container
|
30
29
|
module Notify
|
30
|
+
# Implements a general process readiness protocol with output to the local console.
|
31
31
|
class Console < Client
|
32
|
+
# Open a notification client attached to the current console.
|
32
33
|
def self.open!(logger = ::Console.logger)
|
33
34
|
self.new(logger)
|
34
35
|
end
|
35
36
|
|
37
|
+
# Initialize the notification client.
|
38
|
+
# @parameter logger [Console::Logger] The console logger instance to send messages to.
|
36
39
|
def initialize(logger)
|
37
40
|
@logger = logger
|
38
41
|
end
|
39
42
|
|
43
|
+
# Send a message to the console.
|
40
44
|
def send(level: :debug, **message)
|
41
45
|
@logger.send(level, self) {message}
|
42
46
|
end
|
43
47
|
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
def restarting!(**message)
|
49
|
-
message[:ready] = false
|
50
|
-
message[:reloading] = true
|
51
|
-
message[:status] ||= "Restarting..."
|
52
|
-
|
53
|
-
send(**message)
|
54
|
-
end
|
55
|
-
|
56
|
-
def reloading!(**message)
|
57
|
-
message[:ready] = false
|
58
|
-
message[:reloading] = true
|
59
|
-
message[:status] ||= "Reloading..."
|
60
|
-
|
61
|
-
send(**message)
|
62
|
-
end
|
63
|
-
|
48
|
+
# Send an error message to the console.
|
49
|
+
# @parameters text [String] The details of the error condition.
|
50
|
+
# @parameters message [Hash] Additional details to send with the message.
|
64
51
|
def error!(text, **message)
|
65
52
|
send(status: text, level: :error, **message)
|
66
53
|
end
|
@@ -1,6 +1,5 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
#
|
4
3
|
# Copyright, 2020, by Samuel G. D. Williams. <http://www.codeotaku.com>
|
5
4
|
#
|
6
5
|
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
@@ -28,19 +27,24 @@ require 'json'
|
|
28
27
|
module Async
|
29
28
|
module Container
|
30
29
|
module Notify
|
30
|
+
# Implements a process readiness protocol using an inherited pipe file descriptor.
|
31
31
|
class Pipe < Client
|
32
|
+
# The environment variable key which contains the pipe file descriptor.
|
32
33
|
NOTIFY_PIPE = 'NOTIFY_PIPE'
|
33
34
|
|
35
|
+
# Open a notification client attached to the current {NOTIFY_PIPE} if possible.
|
34
36
|
def self.open!(environment = ENV)
|
35
37
|
if descriptor = environment.delete(NOTIFY_PIPE)
|
36
38
|
self.new(::IO.for_fd(descriptor.to_i))
|
37
39
|
end
|
38
40
|
rescue Errno::EBADF => error
|
39
|
-
|
41
|
+
Console.logger.error(self) {error}
|
40
42
|
|
41
43
|
return nil
|
42
44
|
end
|
43
45
|
|
46
|
+
# Initialize the notification client.
|
47
|
+
# @parameter io [IO] An IO instance used for sending messages.
|
44
48
|
def initialize(io)
|
45
49
|
@io = io
|
46
50
|
end
|
@@ -72,6 +76,8 @@ module Async
|
|
72
76
|
end
|
73
77
|
end
|
74
78
|
|
79
|
+
# Formats the message using JSON and sends it to the parent controller.
|
80
|
+
# This is suitable for use with {Channel}.
|
75
81
|
def send(**message)
|
76
82
|
data = ::JSON.dump(message)
|
77
83
|
|
@@ -79,10 +85,6 @@ module Async
|
|
79
85
|
@io.flush
|
80
86
|
end
|
81
87
|
|
82
|
-
def ready!(**message)
|
83
|
-
send(ready: true, **message)
|
84
|
-
end
|
85
|
-
|
86
88
|
private
|
87
89
|
|
88
90
|
def environment_for(arguments)
|
@@ -1,6 +1,5 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
#
|
4
3
|
# Copyright, 2020, by Samuel G. D. Williams. <http://www.codeotaku.com>
|
5
4
|
#
|
6
5
|
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
@@ -30,21 +29,31 @@ require 'kernel/sync'
|
|
30
29
|
module Async
|
31
30
|
module Container
|
32
31
|
module Notify
|
32
|
+
# Implements the systemd NOTIFY_SOCKET process readiness protocol.
|
33
|
+
# See <https://www.freedesktop.org/software/systemd/man/sd_notify.html> for more details of the underlying protocol.
|
33
34
|
class Socket < Client
|
35
|
+
# The name of the environment variable which contains the path to the notification socket.
|
34
36
|
NOTIFY_SOCKET = 'NOTIFY_SOCKET'
|
37
|
+
|
38
|
+
# The maximum allowed size of the UDP message.
|
35
39
|
MAXIMUM_MESSAGE_SIZE = 4096
|
36
40
|
|
41
|
+
# Open a notification client attached to the current {NOTIFY_SOCKET} if possible.
|
37
42
|
def self.open!(environment = ENV)
|
38
43
|
if path = environment.delete(NOTIFY_SOCKET)
|
39
44
|
self.new(path)
|
40
45
|
end
|
41
46
|
end
|
42
47
|
|
48
|
+
# Initialize the notification client.
|
49
|
+
# @parameter path [String] The path to the UNIX socket used for sending messages to the process manager.
|
43
50
|
def initialize(path)
|
44
51
|
@path = path
|
45
52
|
@endpoint = IO::Endpoint.unix(path, ::Socket::SOCK_DGRAM)
|
46
53
|
end
|
47
54
|
|
55
|
+
# Dump a message in the format requied by `sd_notify`.
|
56
|
+
# @parameter message [Hash] Keys and values should be string convertible objects. Values which are `true`/`false` are converted to `1`/`0` respectively.
|
48
57
|
def dump(message)
|
49
58
|
buffer = String.new
|
50
59
|
|
@@ -62,6 +71,8 @@ module Async
|
|
62
71
|
return buffer
|
63
72
|
end
|
64
73
|
|
74
|
+
# Send the given message.
|
75
|
+
# @parameter message [Hash]
|
65
76
|
def send(**message)
|
66
77
|
data = dump(message)
|
67
78
|
|
@@ -76,6 +87,8 @@ module Async
|
|
76
87
|
end
|
77
88
|
end
|
78
89
|
|
90
|
+
# Send the specified error.
|
91
|
+
# `sd_notify` requires an `errno` key, which defaults to `-1` to indicate a generic error.
|
79
92
|
def error!(text, **message)
|
80
93
|
message[:errno] ||= -1
|
81
94
|
|
@@ -27,8 +27,12 @@ require_relative 'notify/pipe'
|
|
27
27
|
|
28
28
|
module Async
|
29
29
|
module Container
|
30
|
+
# Represents a running child process from the point of view of the parent container.
|
30
31
|
class Process < Channel
|
32
|
+
# Represents a running child process from the point of view of the child process.
|
31
33
|
class Instance < Notify::Pipe
|
34
|
+
# Wrap an instance around the {Process} instance from within the forked child.
|
35
|
+
# @parameter process [Process] The process intance to wrap.
|
32
36
|
def self.for(process)
|
33
37
|
instance = self.new(process.out)
|
34
38
|
|
@@ -46,16 +50,22 @@ module Async
|
|
46
50
|
@name = nil
|
47
51
|
end
|
48
52
|
|
53
|
+
# Set the process title to the specified value.
|
54
|
+
# @parameter value [String] The name of the process.
|
49
55
|
def name= value
|
50
56
|
if @name = value
|
51
57
|
::Process.setproctitle(@name)
|
52
58
|
end
|
53
59
|
end
|
54
60
|
|
61
|
+
# The name of the process.
|
62
|
+
# @returns [String]
|
55
63
|
def name
|
56
64
|
@name
|
57
65
|
end
|
58
66
|
|
67
|
+
# Replace the current child process with a different one. Forwards arguments and options to {::Process.exec}.
|
68
|
+
# This method replaces the child process with the new executable, thus this method never returns.
|
59
69
|
def exec(*arguments, ready: true, **options)
|
60
70
|
if ready
|
61
71
|
self.ready!(status: "(exec)") if ready
|
@@ -63,10 +73,13 @@ module Async
|
|
63
73
|
self.before_spawn(arguments, options)
|
64
74
|
end
|
65
75
|
|
76
|
+
# TODO prefer **options... but it doesn't support redirections on < 2.7
|
66
77
|
::Process.exec(*arguments, options)
|
67
78
|
end
|
68
79
|
end
|
69
80
|
|
81
|
+
# Fork a child process appropriate for a container.
|
82
|
+
# @returns [Process]
|
70
83
|
def self.fork(**options)
|
71
84
|
self.new(**options) do |process|
|
72
85
|
::Process.fork do
|
@@ -78,7 +91,7 @@ module Async
|
|
78
91
|
rescue Interrupt
|
79
92
|
# Graceful exit.
|
80
93
|
rescue Exception => error
|
81
|
-
|
94
|
+
Console.logger.error(self) {error}
|
82
95
|
|
83
96
|
exit!(1)
|
84
97
|
end
|
@@ -96,6 +109,8 @@ module Async
|
|
96
109
|
# end
|
97
110
|
# end
|
98
111
|
|
112
|
+
# Initialize the process.
|
113
|
+
# @parameter name [String] The name to use for the child process.
|
99
114
|
def initialize(name: nil)
|
100
115
|
super()
|
101
116
|
|
@@ -109,6 +124,8 @@ module Async
|
|
109
124
|
self.close_write
|
110
125
|
end
|
111
126
|
|
127
|
+
# Set the name of the process.
|
128
|
+
# Invokes {::Process.setproctitle} if invoked in the child process.
|
112
129
|
def name= value
|
113
130
|
@name = value
|
114
131
|
|
@@ -116,12 +133,17 @@ module Async
|
|
116
133
|
::Process.setproctitle(@name) if @pid.nil?
|
117
134
|
end
|
118
135
|
|
136
|
+
# The name of the process.
|
137
|
+
# @attribute [String]
|
119
138
|
attr :name
|
120
139
|
|
140
|
+
# A human readable representation of the process.
|
141
|
+
# @returns [String]
|
121
142
|
def to_s
|
122
143
|
"\#<#{self.class} #{@name}>"
|
123
144
|
end
|
124
145
|
|
146
|
+
# Invoke {#terminate!} and then {#wait} for the child process to exit.
|
125
147
|
def close
|
126
148
|
self.terminate!
|
127
149
|
self.wait
|
@@ -129,18 +151,22 @@ module Async
|
|
129
151
|
super
|
130
152
|
end
|
131
153
|
|
154
|
+
# Send `SIGINT` to the child process.
|
132
155
|
def interrupt!
|
133
156
|
unless @status
|
134
157
|
::Process.kill(:INT, @pid)
|
135
158
|
end
|
136
159
|
end
|
137
160
|
|
161
|
+
# Send `SIGTERM` to the child process.
|
138
162
|
def terminate!
|
139
163
|
unless @status
|
140
164
|
::Process.kill(:TERM, @pid)
|
141
165
|
end
|
142
166
|
end
|
143
167
|
|
168
|
+
# Wait for the child process to exit.
|
169
|
+
# @returns [::Process::Status] The process exit status.
|
144
170
|
def wait
|
145
171
|
if @pid && @status.nil?
|
146
172
|
_, @status = ::Process.wait2(@pid, ::Process::WNOHANG)
|
@@ -151,7 +177,7 @@ module Async
|
|
151
177
|
end
|
152
178
|
|
153
179
|
if @status.nil?
|
154
|
-
|
180
|
+
Console.logger.warn(self) {"Process #{@pid} is blocking, has it exited?"}
|
155
181
|
_, @status = ::Process.wait2(@pid)
|
156
182
|
end
|
157
183
|
end
|