async-container 0.18.2 → 0.19.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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.2"
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/readme.md CHANGED
@@ -28,8 +28,8 @@ We welcome contributions to this project.
28
28
 
29
29
  ### Developer Certificate of Origin
30
30
 
31
- This project uses the [Developer Certificate of Origin](https://developercertificate.org/). All contributors to this project must agree to this document to have their contributions accepted.
31
+ In order to protect users of this project, we require all contributors to comply with the [Developer Certificate of Origin](https://developercertificate.org/). This ensures that all contributions are properly licensed and attributed.
32
32
 
33
- ### Contributor Covenant
33
+ ### Community Guidelines
34
34
 
35
- This project is governed by the [Contributor Covenant](https://www.contributor-covenant.org/). All contributors and participants agree to abide by its terms.
35
+ This project is best served by a collaborative and respectful environment. Treat each other professionally, respect differing viewpoints, and engage constructively. Harassment, discrimination, or harmful behavior is not tolerated. Communicate clearly, listen actively, and support one another. If any issues arise, please inform the project maintainers.
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.2
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-04-24 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.3
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,173 +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)") if ready
55
- else
56
- self.before_spawn(arguments, options)
57
- end
58
-
59
- # TODO prefer **options... but it doesn't support redirections on < 2.7
60
- ::Process.exec(*arguments, options)
61
- end
62
- end
63
-
64
- # Fork a child process appropriate for a container.
65
- # @returns [Process]
66
- def self.fork(**options)
67
- self.new(**options) do |process|
68
- ::Process.fork do
69
- # We use `Thread.current.raise(...)` so that exceptions are filtered through `Thread.handle_interrupt` correctly.
70
- Signal.trap(:INT) {::Thread.current.raise(Interrupt)}
71
- Signal.trap(:TERM) {::Thread.current.raise(Terminate)}
72
-
73
- begin
74
- yield Instance.for(process)
75
- rescue Interrupt
76
- # Graceful exit.
77
- rescue Exception => error
78
- Console.logger.error(self) {error}
79
-
80
- exit!(1)
81
- end
82
- end
83
- end
84
- end
85
-
86
- # def self.spawn(*arguments, name: nil, **options)
87
- # self.new(name: name) do |process|
88
- # unless options.key?(:out)
89
- # options[:out] = process.out
90
- # end
91
- #
92
- # ::Process.spawn(*arguments, **options)
93
- # end
94
- # end
95
-
96
- # Initialize the process.
97
- # @parameter name [String] The name to use for the child process.
98
- def initialize(name: nil)
99
- super()
100
-
101
- @name = name
102
- @status = nil
103
- @pid = nil
104
-
105
- @pid = yield(self)
106
-
107
- # The parent process won't be writing to the channel:
108
- self.close_write
109
- end
110
-
111
- # Set the name of the process.
112
- # Invokes {::Process.setproctitle} if invoked in the child process.
113
- def name= value
114
- @name = value
115
-
116
- # If we are the child process:
117
- ::Process.setproctitle(@name) if @pid.nil?
118
- end
119
-
120
- # The name of the process.
121
- # @attribute [String]
122
- attr :name
123
-
124
- # A human readable representation of the process.
125
- # @returns [String]
126
- def to_s
127
- "\#<#{self.class} #{@name}>"
128
- end
129
-
130
- # Invoke {#terminate!} and then {#wait} for the child process to exit.
131
- def close
132
- self.terminate!
133
- self.wait
134
- ensure
135
- super
136
- end
137
-
138
- # Send `SIGINT` to the child process.
139
- def interrupt!
140
- unless @status
141
- ::Process.kill(:INT, @pid)
142
- end
143
- end
144
-
145
- # Send `SIGTERM` to the child process.
146
- def terminate!
147
- unless @status
148
- ::Process.kill(:TERM, @pid)
149
- end
150
- end
151
-
152
- # Wait for the child process to exit.
153
- # @returns [::Process::Status] The process exit status.
154
- def wait
155
- if @pid && @status.nil?
156
- _, @status = ::Process.wait2(@pid, ::Process::WNOHANG)
157
-
158
- if @status.nil?
159
- sleep(0.01)
160
- _, @status = ::Process.wait2(@pid, ::Process::WNOHANG)
161
- end
162
-
163
- if @status.nil?
164
- Console.logger.warn(self) {"Process #{@pid} is blocking, has it exited?"}
165
- _, @status = ::Process.wait2(@pid)
166
- end
167
- end
168
-
169
- return @status
170
- end
171
- end
172
- end
173
- end