async-container 0.18.3 → 0.19.0

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.
@@ -1,12 +1,12 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # Released under the MIT License.
4
- # Copyright, 2020-2022, by Samuel Williams.
4
+ # Copyright, 2020-2024, by Samuel Williams.
5
5
  # Copyright, 2020, by Juan Antonio Martín Lucas.
6
6
 
7
- require_relative 'client'
7
+ require_relative "client"
8
8
 
9
- require 'json'
9
+ require "json"
10
10
 
11
11
  module Async
12
12
  module Container
@@ -14,7 +14,7 @@ module Async
14
14
  # Implements a process readiness protocol using an inherited pipe file descriptor.
15
15
  class Pipe < Client
16
16
  # The environment variable key which contains the pipe file descriptor.
17
- NOTIFY_PIPE = 'NOTIFY_PIPE'
17
+ NOTIFY_PIPE = "NOTIFY_PIPE"
18
18
 
19
19
  # Open a notification client attached to the current {NOTIFY_PIPE} if possible.
20
20
  def self.open!(environment = ENV)
@@ -22,7 +22,7 @@ module Async
22
22
  self.new(::IO.for_fd(descriptor.to_i))
23
23
  end
24
24
  rescue Errno::EBADF => error
25
- Console.logger.error(self) {error}
25
+ Console.error(self) {error}
26
26
 
27
27
  return nil
28
28
  end
@@ -4,14 +4,13 @@
4
4
  # Copyright, 2020-2024, by Samuel Williams.
5
5
  # Copyright, 2020, by Olle Jonsson.
6
6
 
7
- require 'tmpdir'
8
- require 'securerandom'
7
+ require "tmpdir"
8
+ require "securerandom"
9
9
 
10
10
  module Async
11
11
  module Container
12
12
  module Notify
13
13
  class Server
14
- NOTIFY_SOCKET = 'NOTIFY_SOCKET'
15
14
  MAXIMUM_MESSAGE_SIZE = 4096
16
15
 
17
16
  def self.load(message)
@@ -22,13 +21,17 @@ module Async
22
21
  pairs = lines.map do |line|
23
22
  key, value = line.split("=", 2)
24
23
 
25
- if value == '0'
24
+ key = key.downcase.to_sym
25
+
26
+ if value == "0"
26
27
  value = false
27
- elsif value == '1'
28
+ elsif value == "1"
28
29
  value = true
30
+ elsif key == :errno and value =~ /\A\-?\d+\z/
31
+ value = Integer(value)
29
32
  end
30
33
 
31
- next [key.downcase.to_sym, value]
34
+ next [key, value]
32
35
  end
33
36
 
34
37
  return Hash[pairs]
@@ -75,7 +78,11 @@ module Async
75
78
 
76
79
  message = Server.load(data)
77
80
 
78
- yield message
81
+ if block_given?
82
+ yield message
83
+ else
84
+ return message
85
+ end
79
86
  end
80
87
  end
81
88
  end
@@ -3,7 +3,8 @@
3
3
  # Released under the MIT License.
4
4
  # Copyright, 2020-2024, by Samuel Williams.
5
5
 
6
- require_relative 'client'
6
+ require_relative "client"
7
+ require "socket"
7
8
 
8
9
  module Async
9
10
  module Container
@@ -12,7 +13,7 @@ module Async
12
13
  # See <https://www.freedesktop.org/software/systemd/man/sd_notify.html> for more details of the underlying protocol.
13
14
  class Socket < Client
14
15
  # The name of the environment variable which contains the path to the notification socket.
15
- NOTIFY_SOCKET = 'NOTIFY_SOCKET'
16
+ NOTIFY_SOCKET = "NOTIFY_SOCKET"
16
17
 
17
18
  # The maximum allowed size of the UDP message.
18
19
  MAXIMUM_MESSAGE_SIZE = 4096
@@ -31,6 +32,9 @@ module Async
31
32
  @address = Addrinfo.unix(path, ::Socket::SOCK_DGRAM)
32
33
  end
33
34
 
35
+ # @attribute [String] The path to the UNIX socket used for sending messages to the controller.
36
+ attr :path
37
+
34
38
  # Dump a message in the format requied by `sd_notify`.
35
39
  # @parameter message [Hash] Keys and values should be string convertible objects. Values which are `true`/`false` are converted to `1`/`0` respectively.
36
40
  def dump(message)
@@ -56,7 +60,7 @@ module Async
56
60
  data = dump(message)
57
61
 
58
62
  if data.bytesize > MAXIMUM_MESSAGE_SIZE
59
- raise ArgumentError, "Message length #{message.bytesize} exceeds #{MAXIMUM_MESSAGE_SIZE}: #{message.inspect}"
63
+ raise ArgumentError, "Message length #{data.bytesize} exceeds #{MAXIMUM_MESSAGE_SIZE}: #{message.inspect}"
60
64
  end
61
65
 
62
66
  @address.connect do |peer|
@@ -69,7 +73,7 @@ module Async
69
73
  def error!(text, **message)
70
74
  message[:errno] ||= -1
71
75
 
72
- send(status: text, **message)
76
+ super
73
77
  end
74
78
  end
75
79
  end
@@ -1,11 +1,11 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # Released under the MIT License.
4
- # Copyright, 2020-2022, by Samuel Williams.
4
+ # Copyright, 2020-2024, by Samuel Williams.
5
5
 
6
- require_relative 'notify/pipe'
7
- require_relative 'notify/socket'
8
- require_relative 'notify/console'
6
+ require_relative "notify/pipe"
7
+ require_relative "notify/socket"
8
+ require_relative "notify/console"
9
9
 
10
10
  module Async
11
11
  module Container
@@ -1,9 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # Released under the MIT License.
4
- # Copyright, 2019-2022, by Samuel Williams.
4
+ # Copyright, 2019-2024, by Samuel Williams.
5
5
 
6
- require 'async/reactor'
6
+ require "async/reactor"
7
7
 
8
8
  module Async
9
9
  module Container
@@ -1,10 +1,11 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # Released under the MIT License.
4
- # Copyright, 2017-2022, by Samuel Williams.
4
+ # Copyright, 2017-2025, by Samuel Williams.
5
5
 
6
- require_relative 'generic'
7
- require_relative 'thread'
6
+ require_relative "generic"
7
+ require_relative "channel"
8
+ require_relative "notify/pipe"
8
9
 
9
10
  module Async
10
11
  module Container
@@ -15,11 +16,202 @@ module Async
15
16
  false
16
17
  end
17
18
 
19
+ # Represents a running child thread from the point of view of the parent container.
20
+ class Child < Channel
21
+ # Used to propagate the exit status of a child process invoked by {Instance#exec}.
22
+ class Exit < Exception
23
+ # Initialize the exit status.
24
+ # @parameter status [::Process::Status] The process exit status.
25
+ def initialize(status)
26
+ @status = status
27
+ end
28
+
29
+ # The process exit status.
30
+ # @attribute [::Process::Status]
31
+ attr :status
32
+
33
+ # The process exit status if it was an error.
34
+ # @returns [::Process::Status | Nil]
35
+ def error
36
+ unless status.success?
37
+ status
38
+ end
39
+ end
40
+ end
41
+
42
+ # Represents a running child thread from the point of view of the child thread.
43
+ class Instance < Notify::Pipe
44
+ # Wrap an instance around the {Thread} instance from within the threaded child.
45
+ # @parameter thread [Thread] The thread intance to wrap.
46
+ def self.for(thread)
47
+ instance = self.new(thread.out)
48
+
49
+ return instance
50
+ end
51
+
52
+ def initialize(io)
53
+ @name = nil
54
+ @thread = ::Thread.current
55
+
56
+ super
57
+ end
58
+
59
+ # Set the name of the thread.
60
+ # @parameter value [String] The name to set.
61
+ def name= value
62
+ @thread.name = value
63
+ end
64
+
65
+ # Get the name of the thread.
66
+ # @returns [String]
67
+ def name
68
+ @thread.name
69
+ end
70
+
71
+ # Execute a child process using {::Process.spawn}. In order to simulate {::Process.exec}, an {Exit} instance is raised to propagage exit status.
72
+ # This creates the illusion that this method does not return (normally).
73
+ def exec(*arguments, ready: true, **options)
74
+ if ready
75
+ self.ready!(status: "(spawn)")
76
+ else
77
+ self.before_spawn(arguments, options)
78
+ end
79
+
80
+ begin
81
+ pid = ::Process.spawn(*arguments, **options)
82
+ ensure
83
+ _, status = ::Process.wait2(pid)
84
+
85
+ raise Exit, status
86
+ end
87
+ end
88
+ end
89
+
90
+ def self.fork(**options)
91
+ self.new(**options) do |thread|
92
+ ::Thread.new do
93
+ yield Instance.for(thread)
94
+ end
95
+ end
96
+ end
97
+
98
+ # Initialize the thread.
99
+ # @parameter name [String] The name to use for the child thread.
100
+ def initialize(name: nil)
101
+ super()
102
+
103
+ @status = nil
104
+
105
+ @thread = yield(self)
106
+ @thread.report_on_exception = false
107
+ @thread.name = name
108
+
109
+ @waiter = ::Thread.new do
110
+ begin
111
+ @thread.join
112
+ rescue Exit => exit
113
+ finished(exit.error)
114
+ rescue Interrupt
115
+ # Graceful shutdown.
116
+ finished
117
+ rescue Exception => error
118
+ finished(error)
119
+ else
120
+ finished
121
+ end
122
+ end
123
+ end
124
+
125
+ # Set the name of the thread.
126
+ # @parameter value [String] The name to set.
127
+ def name= value
128
+ @thread.name = value
129
+ end
130
+
131
+ # Get the name of the thread.
132
+ # @returns [String]
133
+ def name
134
+ @thread.name
135
+ end
136
+
137
+ # A human readable representation of the thread.
138
+ # @returns [String]
139
+ def to_s
140
+ "\#<#{self.class} #{@thread.name}>"
141
+ end
142
+
143
+ # Invoke {#terminate!} and then {#wait} for the child thread to exit.
144
+ def close
145
+ self.terminate!
146
+ self.wait
147
+ ensure
148
+ super
149
+ end
150
+
151
+ # Raise {Interrupt} in the child thread.
152
+ def interrupt!
153
+ @thread.raise(Interrupt)
154
+ end
155
+
156
+ # Raise {Terminate} in the child thread.
157
+ def terminate!
158
+ @thread.raise(Terminate)
159
+ end
160
+
161
+ # Raise {Restart} in the child thread.
162
+ def restart!
163
+ @thread.raise(Restart)
164
+ end
165
+
166
+ # Wait for the thread to exit and return he exit status.
167
+ # @returns [Status]
168
+ def wait
169
+ if @waiter
170
+ @waiter.join
171
+ @waiter = nil
172
+ end
173
+
174
+ return @status
175
+ end
176
+
177
+ # A pseudo exit-status wrapper.
178
+ class Status
179
+ # Initialise the status.
180
+ # @parameter error [::Process::Status] The exit status of the child thread.
181
+ def initialize(error = nil)
182
+ @error = error
183
+ end
184
+
185
+ # Whether the status represents a successful outcome.
186
+ # @returns [Boolean]
187
+ def success?
188
+ @error.nil?
189
+ end
190
+
191
+ # A human readable representation of the status.
192
+ def to_s
193
+ "\#<#{self.class} #{success? ? "success" : "failure"}>"
194
+ end
195
+ end
196
+
197
+ protected
198
+
199
+ # Invoked by the @waiter thread to indicate the outcome of the child thread.
200
+ def finished(error = nil)
201
+ if error
202
+ Console.error(self) {error}
203
+ end
204
+
205
+ @status = Status.new(error)
206
+ self.close_write
207
+ end
208
+ end
209
+
18
210
  # Start a named child thread and execute the provided block in it.
19
211
  # @parameter name [String] The name (title) of the child process.
20
212
  # @parameter block [Proc] The block to execute in the child process.
21
213
  def start(name, &block)
22
- Thread.fork(name: name, &block)
214
+ Child.fork(name: name, &block)
23
215
  end
24
216
  end
25
217
  end
@@ -5,6 +5,6 @@
5
5
 
6
6
  module Async
7
7
  module Container
8
- VERSION = "0.18.3"
8
+ VERSION = "0.19.0"
9
9
  end
10
10
  end
@@ -1,9 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # Released under the MIT License.
4
- # Copyright, 2017-2022, by Samuel Williams.
4
+ # Copyright, 2017-2024, by Samuel Williams.
5
5
 
6
- require_relative 'container/controller'
6
+ require_relative "container/controller"
7
7
 
8
8
  module Async
9
9
  module Container
data/license.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # MIT License
2
2
 
3
- Copyright, 2017-2024, by Samuel Williams.
3
+ Copyright, 2017-2025, by Samuel Williams.
4
4
  Copyright, 2019, by Yuji Yaginuma.
5
5
  Copyright, 2020, by Olle Jonsson.
6
6
  Copyright, 2020, by Juan Antonio Martín Lucas.
data/releases.md ADDED
@@ -0,0 +1,6 @@
1
+ # Releases
2
+
3
+ ## Unreleased
4
+
5
+ - Improve container signal handling reliability by using `Thread.handle_interrupt` except at known safe points.
6
+ - Improved logging when child process fails and container startup.
data.tar.gz.sig CHANGED
Binary file
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: async-container
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.18.3
4
+ version: 0.19.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Samuel Williams
@@ -9,7 +9,6 @@ authors:
9
9
  - Anton Sozontov
10
10
  - Juan Antonio Martín Lucas
11
11
  - Yuji Yaginuma
12
- autorequire:
13
12
  bindir: bin
14
13
  cert_chain:
15
14
  - |
@@ -41,7 +40,7 @@ cert_chain:
41
40
  Q2K9NVun/S785AP05vKkXZEFYxqG6EW012U4oLcFl5MySFajYXRYbuUpH6AY+HP8
42
41
  voD0MPg1DssDLKwXyt1eKD/+Fq0bFWhwVM/1XiAXL7lyYUyOq24KHgQ2Csg=
43
42
  -----END CERTIFICATE-----
44
- date: 2024-09-04 00:00:00.000000000 Z
43
+ date: 2025-02-04 00:00:00.000000000 Z
45
44
  dependencies:
46
45
  - !ruby/object:Gem::Dependency
47
46
  name: async
@@ -57,8 +56,6 @@ dependencies:
57
56
  - - "~>"
58
57
  - !ruby/object:Gem::Version
59
58
  version: '2.10'
60
- description:
61
- email:
62
59
  executables: []
63
60
  extensions: []
64
61
  extra_rdoc_files: []
@@ -79,20 +76,18 @@ files:
79
76
  - lib/async/container/notify/pipe.rb
80
77
  - lib/async/container/notify/server.rb
81
78
  - lib/async/container/notify/socket.rb
82
- - lib/async/container/process.rb
83
79
  - lib/async/container/statistics.rb
84
- - lib/async/container/thread.rb
85
80
  - lib/async/container/threaded.rb
86
81
  - lib/async/container/version.rb
87
82
  - license.md
88
83
  - readme.md
84
+ - releases.md
89
85
  homepage: https://github.com/socketry/async-container
90
86
  licenses:
91
87
  - MIT
92
88
  metadata:
93
89
  documentation_uri: https://socketry.github.io/async-container/
94
90
  source_code_uri: https://github.com/socketry/async-container.git
95
- post_install_message:
96
91
  rdoc_options: []
97
92
  require_paths:
98
93
  - lib
@@ -107,8 +102,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
107
102
  - !ruby/object:Gem::Version
108
103
  version: '0'
109
104
  requirements: []
110
- rubygems_version: 3.5.11
111
- signing_key:
105
+ rubygems_version: 3.6.2
112
106
  specification_version: 4
113
107
  summary: Abstract container-based parallelism using threads and processes where appropriate.
114
108
  test_files: []
metadata.gz.sig CHANGED
Binary file
@@ -1,172 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- # Released under the MIT License.
4
- # Copyright, 2020-2024, by Samuel Williams.
5
-
6
- require_relative 'channel'
7
- require_relative 'error'
8
-
9
- require_relative 'notify/pipe'
10
-
11
- module Async
12
- module Container
13
- # Represents a running child process from the point of view of the parent container.
14
- class Process < Channel
15
- # Represents a running child process from the point of view of the child process.
16
- class Instance < Notify::Pipe
17
- # Wrap an instance around the {Process} instance from within the forked child.
18
- # @parameter process [Process] The process intance to wrap.
19
- def self.for(process)
20
- instance = self.new(process.out)
21
-
22
- # The child process won't be reading from the channel:
23
- process.close_read
24
-
25
- instance.name = process.name
26
-
27
- return instance
28
- end
29
-
30
- def initialize(io)
31
- super
32
-
33
- @name = nil
34
- end
35
-
36
- # Set the process title to the specified value.
37
- # @parameter value [String] The name of the process.
38
- def name= value
39
- if @name = value
40
- ::Process.setproctitle(@name)
41
- end
42
- end
43
-
44
- # The name of the process.
45
- # @returns [String]
46
- def name
47
- @name
48
- end
49
-
50
- # Replace the current child process with a different one. Forwards arguments and options to {::Process.exec}.
51
- # This method replaces the child process with the new executable, thus this method never returns.
52
- def exec(*arguments, ready: true, **options)
53
- if ready
54
- self.ready!(status: "(exec)")
55
- else
56
- self.before_spawn(arguments, options)
57
- end
58
-
59
- ::Process.exec(*arguments, **options)
60
- end
61
- end
62
-
63
- # Fork a child process appropriate for a container.
64
- # @returns [Process]
65
- def self.fork(**options)
66
- self.new(**options) do |process|
67
- ::Process.fork do
68
- # We use `Thread.current.raise(...)` so that exceptions are filtered through `Thread.handle_interrupt` correctly.
69
- Signal.trap(:INT) {::Thread.current.raise(Interrupt)}
70
- Signal.trap(:TERM) {::Thread.current.raise(Terminate)}
71
-
72
- begin
73
- yield Instance.for(process)
74
- rescue Interrupt
75
- # Graceful exit.
76
- rescue Exception => error
77
- Console.logger.error(self) {error}
78
-
79
- exit!(1)
80
- end
81
- end
82
- end
83
- end
84
-
85
- # def self.spawn(*arguments, name: nil, **options)
86
- # self.new(name: name) do |process|
87
- # unless options.key?(:out)
88
- # options[:out] = process.out
89
- # end
90
- #
91
- # ::Process.spawn(*arguments, **options)
92
- # end
93
- # end
94
-
95
- # Initialize the process.
96
- # @parameter name [String] The name to use for the child process.
97
- def initialize(name: nil)
98
- super()
99
-
100
- @name = name
101
- @status = nil
102
- @pid = nil
103
-
104
- @pid = yield(self)
105
-
106
- # The parent process won't be writing to the channel:
107
- self.close_write
108
- end
109
-
110
- # Set the name of the process.
111
- # Invokes {::Process.setproctitle} if invoked in the child process.
112
- def name= value
113
- @name = value
114
-
115
- # If we are the child process:
116
- ::Process.setproctitle(@name) if @pid.nil?
117
- end
118
-
119
- # The name of the process.
120
- # @attribute [String]
121
- attr :name
122
-
123
- # A human readable representation of the process.
124
- # @returns [String]
125
- def to_s
126
- "\#<#{self.class} #{@name}>"
127
- end
128
-
129
- # Invoke {#terminate!} and then {#wait} for the child process to exit.
130
- def close
131
- self.terminate!
132
- self.wait
133
- ensure
134
- super
135
- end
136
-
137
- # Send `SIGINT` to the child process.
138
- def interrupt!
139
- unless @status
140
- ::Process.kill(:INT, @pid)
141
- end
142
- end
143
-
144
- # Send `SIGTERM` to the child process.
145
- def terminate!
146
- unless @status
147
- ::Process.kill(:TERM, @pid)
148
- end
149
- end
150
-
151
- # Wait for the child process to exit.
152
- # @returns [::Process::Status] The process exit status.
153
- def wait
154
- if @pid && @status.nil?
155
- _, @status = ::Process.wait2(@pid, ::Process::WNOHANG)
156
-
157
- if @status.nil?
158
- sleep(0.01)
159
- _, @status = ::Process.wait2(@pid, ::Process::WNOHANG)
160
- end
161
-
162
- if @status.nil?
163
- Console.logger.warn(self) {"Process #{@pid} is blocking, has it exited?"}
164
- _, @status = ::Process.wait2(@pid)
165
- end
166
- end
167
-
168
- return @status
169
- end
170
- end
171
- end
172
- end