fractor 0.1.0 → 0.1.2

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,56 +1,89 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'thread' # Required for Queue
4
-
5
3
  module Fractor
6
4
  # Supervises multiple WrappedRactors, distributes work, and aggregates results.
7
5
  class Supervisor
8
- attr_reader :work_queue, :workers, :results, :worker_class, :work_class
6
+ attr_reader :work_queue, :workers, :results, :worker_pools
9
7
 
10
8
  # Initializes the Supervisor.
11
- # - worker_class: The class inheriting from Fractor::Worker (e.g., MyWorker).
12
- # - work_class: The class inheriting from Fractor::Work (e.g., MyWork).
13
- # - num_workers: The number of Ractors to spawn.
14
- def initialize(worker_class:, work_class:, num_workers: 2)
15
- unless worker_class < Fractor::Worker
16
- raise ArgumentError, "#{worker_class} must inherit from Fractor::Worker"
17
- end
18
- unless work_class < Fractor::Work
19
- raise ArgumentError, "#{work_class} must inherit from Fractor::Work"
9
+ # - worker_pools: An array of worker pool configurations, each containing:
10
+ # - worker_class: The class inheriting from Fractor::Worker (e.g., MyWorker).
11
+ # - num_workers: The number of Ractors to spawn for this worker class.
12
+ # - continuous_mode: Whether to run in continuous mode without expecting a fixed work count.
13
+ def initialize(worker_pools: [], continuous_mode: false)
14
+ @worker_pools = worker_pools.map do |pool_config|
15
+ worker_class = pool_config[:worker_class]
16
+ num_workers = pool_config[:num_workers] || 2
17
+
18
+ raise ArgumentError, "#{worker_class} must inherit from Fractor::Worker" unless worker_class < Fractor::Worker
19
+
20
+ {
21
+ worker_class: worker_class,
22
+ num_workers: num_workers,
23
+ workers: [] # Will hold the WrappedRactor instances
24
+ }
20
25
  end
21
26
 
22
- @worker_class = worker_class
23
- @work_class = work_class
24
27
  @work_queue = Queue.new
25
28
  @results = ResultAggregator.new
26
- @num_workers = num_workers
27
- @workers = []
29
+ @workers = [] # Flattened array of all workers across all pools
28
30
  @total_work_count = 0 # Track total items initially added
29
31
  @ractors_map = {} # Map Ractor object to WrappedRactor instance
32
+ @continuous_mode = continuous_mode
33
+ @running = false
34
+ @work_callbacks = []
35
+ end
36
+
37
+ # Adds a single work item to the queue.
38
+ # The item must be an instance of Fractor::Work or a subclass.
39
+ def add_work_item(work)
40
+ raise ArgumentError, "#{work.class} must be an instance of Fractor::Work" unless work.is_a?(Fractor::Work)
41
+
42
+ @work_queue << work
43
+ @total_work_count += 1
44
+ return unless ENV["FRACTOR_DEBUG"]
45
+
46
+ puts "Work item added. Initial work count: #{@total_work_count}, Queue size: #{@work_queue.size}"
47
+ end
48
+
49
+ # Alias for better naming
50
+ alias add_work_item add_work_item
51
+
52
+ # Adds multiple work items to the queue.
53
+ # Each item must be an instance of Fractor::Work or a subclass.
54
+ def add_work_items(works)
55
+ works.each do |work|
56
+ add_work_item(work)
57
+ end
30
58
  end
31
59
 
32
- # Adds work items to the queue.
33
- # Items should be the raw input data, not Work objects yet.
34
- def add_work(items)
35
- items.each { |item| @work_queue << item }
36
- @total_work_count += items.size
37
- puts "Work added. Initial work count: #{@total_work_count}, Queue size: #{@work_queue.size}"
60
+ # Register a callback to provide new work items
61
+ # The callback should return nil or empty array when no new work is available
62
+ def register_work_source(&callback)
63
+ @work_callbacks << callback
38
64
  end
39
65
 
40
- # Starts the worker Ractors.
66
+ # Starts the worker Ractors for all worker pools.
41
67
  def start_workers
42
- @workers = (1..@num_workers).map do |i|
43
- # Pass the client's worker class (e.g., MyWorker) to WrappedRactor
44
- wrapped_ractor = WrappedRactor.new("worker #{i}", @worker_class)
45
- wrapped_ractor.start # Start the underlying Ractor
46
- # Map the actual Ractor object to the WrappedRactor instance
47
- @ractors_map[wrapped_ractor.ractor] = wrapped_ractor if wrapped_ractor.ractor
48
- wrapped_ractor
68
+ @worker_pools.each do |pool|
69
+ worker_class = pool[:worker_class]
70
+ num_workers = pool[:num_workers]
71
+
72
+ pool[:workers] = (1..num_workers).map do |i|
73
+ wrapped_ractor = WrappedRactor.new("worker #{worker_class}:#{i}", worker_class)
74
+ wrapped_ractor.start # Start the underlying Ractor
75
+ # Map the actual Ractor object to the WrappedRactor instance
76
+ @ractors_map[wrapped_ractor.ractor] = wrapped_ractor if wrapped_ractor.ractor
77
+ wrapped_ractor
78
+ end.compact
49
79
  end
50
- # Filter out any workers that failed to start properly
51
- @workers.compact!
80
+
81
+ # Flatten all workers for easier access
82
+ @workers = @worker_pools.flat_map { |pool| pool[:workers] }
52
83
  @ractors_map.compact! # Ensure map doesn't contain nil keys/values
53
- puts "Workers started: #{@workers.size} active."
84
+ return unless ENV["FRACTOR_DEBUG"]
85
+
86
+ puts "Workers started: #{@workers.size} active across #{@worker_pools.size} pools."
54
87
  end
55
88
 
56
89
  # Sets up a signal handler for graceful shutdown (Ctrl+C).
@@ -61,12 +94,10 @@ module Fractor
61
94
  puts "\nCtrl+C received. Initiating immediate shutdown..."
62
95
  puts "Attempting to close worker Ractors..."
63
96
  workers_ref.each do |w|
64
- begin
65
- w.close # Use the close method of WrappedRactor
66
- puts "Closed Ractor: #{w.name}"
67
- rescue => e
68
- puts "Error closing Ractor #{w.name}: #{e.message}"
69
- end
97
+ w.close # Use the close method of WrappedRactor
98
+ puts "Closed Ractor: #{w.name}"
99
+ rescue StandardError => e
100
+ puts "Error closing Ractor #{w.name}: #{e.message}"
70
101
  end
71
102
  puts "Exiting now."
72
103
  exit(1) # Exit immediately
@@ -78,24 +109,46 @@ module Fractor
78
109
  setup_signal_handler
79
110
  start_workers
80
111
 
112
+ @running = true
81
113
  processed_count = 0
82
- # Main loop: Process events until the number of results equals the initial work count.
83
- while processed_count < @total_work_count
114
+
115
+ # Main loop: Process events until conditions are met for termination
116
+ while @running && (@continuous_mode || processed_count < @total_work_count)
84
117
  processed_count = @results.results.size + @results.errors.size
85
- puts "Waiting for Ractor results. Processed: #{processed_count}/#{@total_work_count}, Queue size: #{@work_queue.size}"
118
+
119
+ if ENV["FRACTOR_DEBUG"]
120
+ if @continuous_mode
121
+ puts "Continuous mode: Waiting for Ractor results. Processed: #{processed_count}, Queue size: #{@work_queue.size}"
122
+ else
123
+ puts "Waiting for Ractor results. Processed: #{processed_count}/#{@total_work_count}, Queue size: #{@work_queue.size}"
124
+ end
125
+ end
86
126
 
87
127
  # Get active Ractor objects from the map keys
88
- # Use keys from ractors_map for the active ractors
89
128
  active_ractors = @ractors_map.keys
90
129
 
130
+ # Check for new work from callbacks if in continuous mode and queue is empty
131
+ if @continuous_mode && @work_queue.empty? && !@work_callbacks.empty?
132
+ @work_callbacks.each do |callback|
133
+ new_work = callback.call
134
+ add_work_items(new_work) if new_work && !new_work.empty?
135
+ end
136
+ end
137
+
91
138
  # Break if no active workers and queue is empty, but work remains (indicates potential issue)
92
- if active_ractors.empty? && @work_queue.empty? && processed_count < @total_work_count
93
- puts "Warning: No active workers and queue is empty, but not all work is processed. Exiting loop."
94
- break
139
+ if active_ractors.empty? && @work_queue.empty? && !@continuous_mode && processed_count < @total_work_count
140
+ puts "Warning: No active workers and queue is empty, but not all work is processed. Exiting loop." if ENV["FRACTOR_DEBUG"]
141
+ break
95
142
  end
96
143
 
97
- # Skip selection if no active ractors are available but loop should continue (e.g., waiting for final results)
98
- next if active_ractors.empty?
144
+ # In continuous mode, just wait if no active ractors but keep running
145
+ if active_ractors.empty?
146
+ break unless @continuous_mode
147
+
148
+ sleep(0.1) # Small delay to avoid CPU spinning
149
+ next
150
+
151
+ end
99
152
 
100
153
  # Ractor.select blocks until a message is available from any active Ractor
101
154
  ready_ractor_obj, message = Ractor.select(*active_ractors)
@@ -103,47 +156,61 @@ module Fractor
103
156
  # Find the corresponding WrappedRactor instance
104
157
  wrapped_ractor = @ractors_map[ready_ractor_obj]
105
158
  unless wrapped_ractor
106
- puts "Warning: Received message from unknown Ractor: #{ready_ractor_obj}. Ignoring."
159
+ puts "Warning: Received message from unknown Ractor: #{ready_ractor_obj}. Ignoring." if ENV["FRACTOR_DEBUG"]
107
160
  next
108
161
  end
109
162
 
110
- puts "Selected Ractor: #{wrapped_ractor.name}, Message Type: #{message[:type]}"
163
+ puts "Selected Ractor: #{wrapped_ractor.name}, Message Type: #{message[:type]}" if ENV["FRACTOR_DEBUG"]
111
164
 
112
165
  # Process the received message
113
166
  case message[:type]
114
167
  when :initialize
115
- puts "Ractor initialized: #{message[:processor]}"
168
+ puts "Ractor initialized: #{message[:processor]}" if ENV["FRACTOR_DEBUG"]
116
169
  # Send work immediately upon initialization if available
117
170
  send_next_work_if_available(wrapped_ractor)
118
171
  when :result
119
172
  # The message[:result] should be a WorkResult object
120
173
  work_result = message[:result]
121
- puts "Completed work: #{work_result.inspect} in Ractor: #{message[:processor]}"
174
+ puts "Completed work: #{work_result.inspect} in Ractor: #{message[:processor]}" if ENV["FRACTOR_DEBUG"]
122
175
  @results.add_result(work_result)
123
- puts "Result processed. Total processed: #{@results.results.size + @results.errors.size}/#{@total_work_count}"
124
- puts "Aggregated Results: #{@results.inspect}"
176
+ if ENV["FRACTOR_DEBUG"]
177
+ puts "Result processed. Total processed: #{@results.results.size + @results.errors.size}"
178
+ puts "Aggregated Results: #{@results.inspect}" unless @continuous_mode
179
+ end
125
180
  # Send next piece of work
126
181
  send_next_work_if_available(wrapped_ractor)
127
182
  when :error
128
183
  # The message[:result] should be a WorkResult object containing the error
129
184
  error_result = message[:result]
130
- puts "Error processing work #{error_result.work&.inspect} in Ractor: #{message[:processor]}: #{error_result.error}"
185
+ puts "Error processing work #{error_result.work&.inspect} in Ractor: #{message[:processor]}: #{error_result.error}" if ENV["FRACTOR_DEBUG"]
131
186
  @results.add_result(error_result) # Add error to aggregator
132
- puts "Error handled. Total processed: #{@results.results.size + @results.errors.size}/#{@total_work_count}"
133
- puts "Aggregated Results (including errors): #{@results.inspect}"
187
+ if ENV["FRACTOR_DEBUG"]
188
+ puts "Error handled. Total processed: #{@results.results.size + @results.errors.size}"
189
+ puts "Aggregated Results (including errors): #{@results.inspect}" unless @continuous_mode
190
+ end
134
191
  # Send next piece of work even after an error
135
192
  send_next_work_if_available(wrapped_ractor)
136
193
  else
137
- puts "Unknown message type received: #{message[:type]} from #{wrapped_ractor.name}"
194
+ puts "Unknown message type received: #{message[:type]} from #{wrapped_ractor.name}" if ENV["FRACTOR_DEBUG"]
138
195
  end
139
196
  # Update processed count for the loop condition
140
197
  processed_count = @results.results.size + @results.errors.size
141
198
  end
142
199
 
143
- puts "Main loop finished."
200
+ puts "Main loop finished." if ENV["FRACTOR_DEBUG"]
201
+ return if @continuous_mode
202
+
203
+ return unless ENV["FRACTOR_DEBUG"]
204
+
144
205
  puts "Final Aggregated Results: #{@results.inspect}"
145
206
  end
146
207
 
208
+ # Stop the supervisor (for continuous mode)
209
+ def stop
210
+ @running = false
211
+ puts "Stopping supervisor..."
212
+ end
213
+
147
214
  private
148
215
 
149
216
  # Helper method to send the next available work item to a specific Ractor.
@@ -151,21 +218,25 @@ module Fractor
151
218
  # Ensure the wrapped_ractor instance is valid and its underlying ractor is not closed
152
219
  if wrapped_ractor && !wrapped_ractor.closed?
153
220
  if !@work_queue.empty?
154
- raw_input = @work_queue.pop # Get raw input data
155
- # Create an instance of the client's Work class (e.g., MyWork)
156
- work_item = @work_class.new(raw_input)
157
- puts "Sending next work #{work_item.inspect} to Ractor: #{wrapped_ractor.name}"
221
+ work_item = @work_queue.pop # Now directly a Work object
222
+
223
+ puts "Sending next work #{work_item.inspect} to Ractor: #{wrapped_ractor.name}" if ENV["FRACTOR_DEBUG"]
158
224
  wrapped_ractor.send(work_item) # Send the Work object
159
- puts "Work sent to #{wrapped_ractor.name}."
225
+ puts "Work sent to #{wrapped_ractor.name}." if ENV["FRACTOR_DEBUG"]
160
226
  else
161
- puts "Work queue empty. Not sending new work to Ractor #{wrapped_ractor.name}."
162
- # Consider closing the Ractor if the queue is empty and no more work is expected.
163
- # wrapped_ractor.close
164
- # @ractors_map.delete(wrapped_ractor.ractor)
165
- # puts "Closed idle Ractor: #{wrapped_ractor.name}"
227
+ puts "Work queue empty. Not sending new work to Ractor #{wrapped_ractor.name}." if ENV["FRACTOR_DEBUG"]
228
+ # In continuous mode, don't close workers as more work may come
229
+ unless @continuous_mode
230
+ # Consider closing the Ractor if the queue is empty and no more work is expected.
231
+ # wrapped_ractor.close
232
+ # @ractors_map.delete(wrapped_ractor.ractor)
233
+ # if ENV["FRACTOR_DEBUG"]
234
+ # puts "Closed idle Ractor: #{wrapped_ractor.name}"
235
+ # end
236
+ end
166
237
  end
167
238
  else
168
- puts "Attempted to send work to an invalid or closed Ractor: #{wrapped_ractor&.name || 'unknown'}."
239
+ puts "Attempted to send work to an invalid or closed Ractor: #{wrapped_ractor&.name || "unknown"}." if ENV["FRACTOR_DEBUG"]
169
240
  # Remove from map if found but closed
170
241
  @ractors_map.delete(wrapped_ractor.ractor) if wrapped_ractor && @ractors_map.key?(wrapped_ractor.ractor)
171
242
  end
@@ -2,5 +2,5 @@
2
2
 
3
3
  module Fractor
4
4
  # Fractor version
5
- VERSION = "0.1.0"
5
+ VERSION = "0.1.2"
6
6
  end
@@ -4,6 +4,11 @@ module Fractor
4
4
  # Base class for defining work processors.
5
5
  # Subclasses must implement the `process` method.
6
6
  class Worker
7
+ def initialize(name: nil, **options)
8
+ @name = name
9
+ @options = options
10
+ end
11
+
7
12
  def process(work)
8
13
  raise NotImplementedError, "Subclasses must implement the 'process' method."
9
14
  end
@@ -25,7 +25,7 @@ module Fractor
25
25
  Ractor.yield({ type: :initialize, processor: name })
26
26
 
27
27
  # Instantiate the specific worker inside the Ractor
28
- worker = worker_cls.new
28
+ worker = worker_cls.new(name: name)
29
29
 
30
30
  loop do
31
31
  # Ractor.receive will block until a message is received
data/lib/fractor.rb CHANGED
@@ -1,15 +1,13 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'thread' # Required for Queue
4
-
5
3
  # Require all component files
6
- require_relative 'fractor/version'
7
- require_relative 'fractor/worker'
8
- require_relative 'fractor/work'
9
- require_relative 'fractor/work_result'
10
- require_relative 'fractor/result_aggregator'
11
- require_relative 'fractor/wrapped_ractor'
12
- require_relative 'fractor/supervisor'
4
+ require_relative "fractor/version"
5
+ require_relative "fractor/worker"
6
+ require_relative "fractor/work"
7
+ require_relative "fractor/work_result"
8
+ require_relative "fractor/result_aggregator"
9
+ require_relative "fractor/wrapped_ractor"
10
+ require_relative "fractor/supervisor"
13
11
 
14
12
  # Fractor: Function-driven Ractors framework
15
13
  module Fractor
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: fractor
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.1.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ronald Tse
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2025-05-05 00:00:00.000000000 Z
11
+ date: 2025-05-08 00:00:00.000000000 Z
12
12
  dependencies: []
13
13
  description: Fractor is a lightweight Ruby framework designed to simplify the process
14
14
  of distributing computational work across multiple Ractors.
@@ -20,11 +20,23 @@ extra_rdoc_files: []
20
20
  files:
21
21
  - ".rspec"
22
22
  - ".rubocop.yml"
23
+ - ".rubocop_todo.yml"
23
24
  - CODE_OF_CONDUCT.md
24
25
  - README.adoc
25
26
  - Rakefile
26
- - examples/hierarchical_hasher.rb
27
- - examples/producer_subscriber.rb
27
+ - examples/hierarchical_hasher/README.adoc
28
+ - examples/hierarchical_hasher/hierarchical_hasher.rb
29
+ - examples/multi_work_type/README.adoc
30
+ - examples/multi_work_type/multi_work_type.rb
31
+ - examples/pipeline_processing/README.adoc
32
+ - examples/pipeline_processing/pipeline_processing.rb
33
+ - examples/producer_subscriber/README.adoc
34
+ - examples/producer_subscriber/producer_subscriber.rb
35
+ - examples/scatter_gather/README.adoc
36
+ - examples/scatter_gather/scatter_gather.rb
37
+ - examples/simple/sample.rb
38
+ - examples/specialized_workers/README.adoc
39
+ - examples/specialized_workers/specialized_workers.rb
28
40
  - lib/fractor.rb
29
41
  - lib/fractor/result_aggregator.rb
30
42
  - lib/fractor/supervisor.rb
@@ -33,7 +45,6 @@ files:
33
45
  - lib/fractor/work_result.rb
34
46
  - lib/fractor/worker.rb
35
47
  - lib/fractor/wrapped_ractor.rb
36
- - sample.rb
37
48
  - sig/fractor.rbs
38
49
  - tests/sample.rb.bak
39
50
  - tests/sample_working.rb.bak
@@ -1,158 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'digest'
4
- require_relative '../lib/fractor'
5
-
6
- module HierarchicalHasher
7
- class ChunkWork < Fractor::Work
8
- work_type :chunk_hash
9
- attr_reader :start, :length, :data
10
-
11
- def initialize(start, length, data)
12
- super(nil, { start: start, length: length, data: data })
13
- @start = start
14
- @length = length
15
- @data = data
16
- @retry_count = 0
17
- @max_retries = 3
18
- end
19
-
20
- def should_retry?
21
- @retry_count < @max_retries
22
- end
23
-
24
- def failed
25
- @retry_count += 1
26
- end
27
- end
28
-
29
- class ChunkResult < Fractor::WorkResult
30
- attr_reader :start, :length, :hash_result
31
-
32
- def initialize(work, hash_result)
33
- super(work, hash_result)
34
- @start = work.start
35
- @length = work.length
36
- @hash_result = hash_result
37
- end
38
- end
39
-
40
- class HashWorker < Fractor::Worker
41
- work_type_accepted :chunk_hash
42
-
43
- def process_work(work)
44
- # Simulate some processing time
45
- sleep(rand(0.01..0.05))
46
-
47
- # Calculate SHA-3 hash for the chunk
48
- hash = Digest::SHA3.hexdigest(work.data)
49
-
50
- # Return the result
51
- ChunkResult.new(work, hash)
52
- end
53
- end
54
-
55
- class HashResultAssembler < Fractor::ResultAssembler
56
- def finalize
57
- return nil if @results.empty?
58
-
59
- # Sort results by start position
60
- sorted_results = @results.sort_by { |result| result.start }
61
-
62
- # Concatenate all hashes with newlines
63
- combined_hash_string = sorted_results.map(&:hash_result).join("\n")
64
-
65
- # Calculate final SHA-3 hash
66
- Digest::SHA3.hexdigest(combined_hash_string)
67
- end
68
- end
69
-
70
- class HashSupervisor < Fractor::Supervisor
71
- def initialize(file_path, chunk_size = 1024, worker_count = 4)
72
- super()
73
- @file_path = file_path
74
- @chunk_size = chunk_size
75
- @worker_count = worker_count
76
- @assembler = HashResultAssembler.new
77
-
78
- # Create a queue for chunk work
79
- chunk_queue = Fractor::Queue.new(work_types: [:chunk_hash])
80
- add_queue(chunk_queue)
81
-
82
- # Create a worker pool
83
- worker_pool = Fractor::Pool.new(size: @worker_count)
84
- @worker_count.times do
85
- worker_pool.add_worker(HashWorker.new)
86
- end
87
- add_pool(worker_pool)
88
-
89
- # Load the file and create work chunks
90
- load_file_chunks
91
- end
92
-
93
- def load_file_chunks
94
- File.open(@file_path, 'rb') do |file|
95
- start_pos = 0
96
-
97
- while chunk = file.read(@chunk_size)
98
- work = ChunkWork.new(start_pos, chunk.length, chunk)
99
- @queues.first.push(work)
100
- start_pos += chunk.length
101
- end
102
- end
103
- end
104
-
105
- def process_results
106
- until @result_queue.empty? && @queues.all?(&:empty?) && @idle_workers == @active_workers.size
107
- result_data = next_result
108
- next if result_data.nil?
109
-
110
- type, *data = result_data
111
-
112
- case type
113
- when :result
114
- result = data.first
115
- @assembler.add_result(result)
116
- when :error
117
- work, error = data
118
- @assembler.add_failed_work(work, error)
119
- end
120
-
121
- # Small sleep to prevent CPU spinning
122
- sleep 0.001
123
- end
124
-
125
- # Return the final hash
126
- @assembler.finalize
127
- end
128
- end
129
- end
130
-
131
- # Example usage
132
- if __FILE__ == $PROGRAM_NAME
133
- if ARGV.empty?
134
- puts "Usage: ruby hierarchical_hasher.rb <file_path> [worker_count]"
135
- exit 1
136
- end
137
-
138
- file_path = ARGV[0]
139
- worker_count = (ARGV[1] || 4).to_i
140
-
141
- unless File.exist?(file_path)
142
- puts "Error: File '#{file_path}' not found"
143
- exit 1
144
- end
145
-
146
- puts "Starting hierarchical hasher with #{worker_count} workers..."
147
- puts "Processing file: #{file_path}"
148
-
149
- start_time = Time.now
150
- supervisor = HierarchicalHasher::HashSupervisor.new(file_path, 1024, worker_count)
151
- final_hash = supervisor.start
152
- end_time = Time.now
153
-
154
- puts "Final SHA-3 hash: #{final_hash}"
155
- puts "Processing completed in #{end_time - start_time} seconds"
156
-
157
- supervisor.shutdown
158
- end