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.
@@ -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
- # This method sleeps for at most the specified duration.
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
- Async.logger.debug(self, "Sending interrupt to #{@running.size} running processes...")
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
- Async.logger.debug(self, "Sending terminate to #{@running.size} running processes...")
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
- # We cache the client on a per-process basis. Because that's the relevant scope for process readiness protocols.
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
- # Select the best available client:
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
- def ready!(**message)
45
- send(ready: true, **message)
46
- end
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
- Async.logger.error(self) {error}
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
@@ -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
- Async.logger.error(self) {error}
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
- Async.logger.warn(self) {"Process #{@pid} is blocking, has it exited?"}
180
+ Console.logger.warn(self) {"Process #{@pid} is blocking, has it exited?"}
155
181
  _, @status = ::Process.wait2(@pid)
156
182
  end
157
183
  end