async-container 0.19.0 → 0.20.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 2edeb6fa679f328736bb4a65c3b342b4aea94f2fc79557d4fc6e9e84bee77a2e
4
- data.tar.gz: 5c1c52a4f90c8710efebb411d97e5a8342ea99d1bbe09082deed562b8acdce99
3
+ metadata.gz: a95321eeaead4fafe8c8db6bfa186d13559c8b39185c566db474ff2111e3add8
4
+ data.tar.gz: 873a71e53157cf3f5c94908daff4848f88b0bb06c35e10fd89c569fe6a646386
5
5
  SHA512:
6
- metadata.gz: ee365ff16248e3064b136cdfeddec8e0f01ef77decbd514b94fc4a3bce1a38c2c51ffa5ba1e071b2fcd6bad6b7c4d4e322f26e8e5a530e4f45a68266d67429da
7
- data.tar.gz: 171a93c5855cad3a112622232bbc7e93b880d4c77223cb8a17c7772a0687a0be40af3143a0ecad2b14a9e9aa656010cdf47eb42f80e14b08148cd02f4a4eaa6c
6
+ metadata.gz: e974514dc85a284427791fffa2299c1541f4330b60f4d1470a64c9d2591d9c9c5a59250d9560e4b61ca43024869681f5f2cdc89ab086b178a391105318691b9d
7
+ data.tar.gz: 712370e1ea844ede123bb6215422c393109e50133db7991242b748f013e75904494af9300fdcbcd5de490c65c473fb195eac46eabbb5d5f34e1a774235cf5e8e
checksums.yaml.gz.sig CHANGED
Binary file
@@ -116,6 +116,24 @@ module Async
116
116
  self.close_write
117
117
  end
118
118
 
119
+ # Convert the child process to a hash, suitable for serialization.
120
+ #
121
+ # @returns [Hash] The request as a hash.
122
+ def as_json(...)
123
+ {
124
+ name: @name,
125
+ pid: @pid,
126
+ status: @status&.to_i,
127
+ }
128
+ end
129
+
130
+ # Convert the request to JSON.
131
+ #
132
+ # @returns [String] The request as JSON.
133
+ def to_json(...)
134
+ as_json.to_json(...)
135
+ end
136
+
119
137
  # Set the name of the process.
120
138
  # Invokes {::Process.setproctitle} if invoked in the child process.
121
139
  def name= value
@@ -4,6 +4,7 @@
4
4
  # Copyright, 2019-2024, by Samuel Williams.
5
5
 
6
6
  require "etc"
7
+ require "async/clock"
7
8
 
8
9
  require_relative "group"
9
10
  require_relative "keyed"
@@ -141,7 +142,8 @@ module Async
141
142
  # @parameter name [String] The name of the child instance.
142
143
  # @parameter restart [Boolean] Whether to restart the child instance if it fails.
143
144
  # @parameter key [Symbol] A key used for reloading child instances.
144
- def spawn(name: nil, restart: false, key: nil, &block)
145
+ # @parameter health_check_timeout [Numeric | Nil] The maximum time a child instance can run without updating its state, before it is terminated as unhealthy.
146
+ def spawn(name: nil, restart: false, key: nil, health_check_timeout: nil, &block)
145
147
  name ||= UNNAMED
146
148
 
147
149
  if mark?(key)
@@ -157,9 +159,24 @@ module Async
157
159
 
158
160
  state = insert(key, child)
159
161
 
162
+ # If a health check is specified, we will monitor the child process and terminate it if it does not update its state within the specified time.
163
+ if health_check_timeout
164
+ age_clock = state[:age] = Clock.start
165
+ end
166
+
160
167
  begin
161
168
  status = @group.wait_for(child) do |message|
162
- state.update(message)
169
+ case message
170
+ when :health_check!
171
+ if health_check_timeout&.<(age_clock.total)
172
+ Console.warn(self, "Child failed health check!", child: child, age: age_clock.total, health_check_timeout: health_check_timeout)
173
+ # If the child has failed the health check, we assume the worst and terminate it (SIGTERM).
174
+ child.terminate!
175
+ end
176
+ else
177
+ state.update(message)
178
+ age_clock&.reset!
179
+ end
163
180
  end
164
181
  ensure
165
182
  delete(key, child)
@@ -13,7 +13,12 @@ module Async
13
13
  # Manages a group of running processes.
14
14
  class Group
15
15
  # Initialize an empty group.
16
- def initialize
16
+ #
17
+ # @parameter health_check_interval [Numeric | Nil] The (biggest) interval at which health checks are performed.
18
+ def initialize(health_check_interval: 1.0)
19
+ @health_check_interval = health_check_interval
20
+
21
+ # The running fibers, indexed by IO:
17
22
  @running = {}
18
23
 
19
24
  # This queue allows us to wait for processes to complete, without spawning new processes as a result.
@@ -57,8 +62,36 @@ module Async
57
62
  def wait
58
63
  self.resume
59
64
 
60
- while self.running?
61
- self.wait_for_children
65
+ with_health_checks do |duration|
66
+ self.wait_for_children(duration)
67
+ end
68
+ end
69
+
70
+ private def with_health_checks
71
+ if @health_check_interval
72
+ health_check_clock = Clock.start
73
+
74
+ while self.running?
75
+ duration = [@health_check_interval - health_check_clock.total, 0].max
76
+
77
+ yield duration
78
+
79
+ if health_check_clock.total > @health_check_interval
80
+ self.health_check!
81
+ health_check_clock.reset!
82
+ end
83
+ end
84
+ else
85
+ while self.running?
86
+ yield nil
87
+ end
88
+ end
89
+ end
90
+
91
+ # Perform a health check on all running processes.
92
+ def health_check!
93
+ @running.each_value do |fiber|
94
+ fiber.resume(:health_check!)
62
95
  end
63
96
  end
64
97
 
@@ -119,15 +152,19 @@ module Async
119
152
  @running[io] = Fiber.current
120
153
 
121
154
  while @running.key?(io)
155
+ # Wait for some event on the channel:
122
156
  result = Fiber.yield
123
157
 
124
158
  if result == Interrupt
125
159
  channel.interrupt!
126
160
  elsif result == Terminate
127
161
  channel.terminate!
162
+ elsif result
163
+ yield result
128
164
  elsif message = channel.receive
129
165
  yield message
130
166
  else
167
+ # Wait for the channel to exit:
131
168
  return channel.wait
132
169
  end
133
170
  end
@@ -90,7 +90,10 @@ module Async
90
90
  def self.fork(**options)
91
91
  self.new(**options) do |thread|
92
92
  ::Thread.new do
93
- yield Instance.for(thread)
93
+ # This could be a configuration option (see forked implementation too):
94
+ ::Thread.handle_interrupt(SignalException => :immediate) do
95
+ yield Instance.for(thread)
96
+ end
94
97
  end
95
98
  end
96
99
  end
@@ -122,6 +125,23 @@ module Async
122
125
  end
123
126
  end
124
127
 
128
+ # Convert the child process to a hash, suitable for serialization.
129
+ #
130
+ # @returns [Hash] The request as a hash.
131
+ def as_json(...)
132
+ {
133
+ name: @thread.name,
134
+ status: @status&.as_json,
135
+ }
136
+ end
137
+
138
+ # Convert the request to JSON.
139
+ #
140
+ # @returns [String] The request as JSON.
141
+ def to_json(...)
142
+ as_json.to_json(...)
143
+ end
144
+
125
145
  # Set the name of the thread.
126
146
  # @parameter value [String] The name to set.
127
147
  def name= value
@@ -188,6 +208,14 @@ module Async
188
208
  @error.nil?
189
209
  end
190
210
 
211
+ def as_json(...)
212
+ if @error
213
+ @error.inspect
214
+ else
215
+ true
216
+ end
217
+ end
218
+
191
219
  # A human readable representation of the status.
192
220
  def to_s
193
221
  "\#<#{self.class} #{success? ? "success" : "failure"}>"
@@ -5,6 +5,6 @@
5
5
 
6
6
  module Async
7
7
  module Container
8
- VERSION = "0.19.0"
8
+ VERSION = "0.20.0"
9
9
  end
10
10
  end
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.19.0
4
+ version: 0.20.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Samuel Williams
@@ -40,7 +40,7 @@ cert_chain:
40
40
  Q2K9NVun/S785AP05vKkXZEFYxqG6EW012U4oLcFl5MySFajYXRYbuUpH6AY+HP8
41
41
  voD0MPg1DssDLKwXyt1eKD/+Fq0bFWhwVM/1XiAXL7lyYUyOq24KHgQ2Csg=
42
42
  -----END CERTIFICATE-----
43
- date: 2025-02-04 00:00:00.000000000 Z
43
+ date: 2025-02-07 00:00:00.000000000 Z
44
44
  dependencies:
45
45
  - !ruby/object:Gem::Dependency
46
46
  name: async
@@ -48,14 +48,14 @@ dependencies:
48
48
  requirements:
49
49
  - - "~>"
50
50
  - !ruby/object:Gem::Version
51
- version: '2.10'
51
+ version: '2.22'
52
52
  type: :runtime
53
53
  prerelease: false
54
54
  version_requirements: !ruby/object:Gem::Requirement
55
55
  requirements:
56
56
  - - "~>"
57
57
  - !ruby/object:Gem::Version
58
- version: '2.10'
58
+ version: '2.22'
59
59
  executables: []
60
60
  extensions: []
61
61
  extra_rdoc_files: []
metadata.gz.sig CHANGED
Binary file