async-container 0.18.3 → 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.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