fractor 0.1.0 → 0.1.1

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.
@@ -0,0 +1,150 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../../lib/fractor"
4
+ require "digest/sha2" # Using SHA2 which is more Ractor-compatible
5
+
6
+ module HierarchicalHasher
7
+ # Define our Work class
8
+ class ChunkWork < Fractor::Work
9
+ def initialize(data, start = 0, length = nil)
10
+ super({
11
+ data: data,
12
+ start: start,
13
+ length: length || data.bytesize
14
+ })
15
+ end
16
+
17
+ def data
18
+ input[:data]
19
+ end
20
+
21
+ def start
22
+ input[:start]
23
+ end
24
+
25
+ def length
26
+ input[:length]
27
+ end
28
+
29
+ def to_s
30
+ "ChunkWork: start=#{start}, length=#{length}, data_size=#{data.bytesize}"
31
+ end
32
+ end
33
+
34
+ # Define our Worker class
35
+ class HashWorker < Fractor::Worker
36
+ def process(work)
37
+ # Simulate some processing time
38
+ sleep(rand(0.01..0.05))
39
+
40
+ # Calculate SHA-256 hash for the chunk (using SHA2 which is Ractor-compatible)
41
+ begin
42
+ hash = Digest::SHA256.hexdigest(work.data)
43
+
44
+ # Return successful result
45
+ Fractor::WorkResult.new(
46
+ result: {
47
+ start: work.start,
48
+ length: work.length,
49
+ hash: hash
50
+ },
51
+ work: work
52
+ )
53
+ rescue StandardError => e
54
+ # Return error result if something goes wrong
55
+ Fractor::WorkResult.new(
56
+ error: "Failed to hash chunk: #{e.message}",
57
+ work: work
58
+ )
59
+ end
60
+ end
61
+ end
62
+
63
+ class FileHasher
64
+ attr_reader :file_path, :chunk_size, :final_hash
65
+
66
+ def initialize(file_path, chunk_size = 1024, worker_count = 4)
67
+ @file_path = file_path
68
+ @chunk_size = chunk_size
69
+ @worker_count = worker_count
70
+ @final_hash = nil
71
+ end
72
+
73
+ def hash_file
74
+ # Create the supervisor with our worker class in a worker pool
75
+ supervisor = Fractor::Supervisor.new(
76
+ worker_pools: [
77
+ { worker_class: HashWorker, num_workers: @worker_count }
78
+ ]
79
+ )
80
+
81
+ # Load the file and create work chunks
82
+ load_file_chunks(supervisor)
83
+
84
+ # Run the processing
85
+ supervisor.run
86
+
87
+ # Process the results to get the final hash
88
+ @final_hash = finalize_hash(supervisor.results)
89
+
90
+ @final_hash
91
+ end
92
+
93
+ private
94
+
95
+ def load_file_chunks(supervisor)
96
+ work_items = []
97
+
98
+ File.open(@file_path, "rb") do |file|
99
+ start_pos = 0
100
+
101
+ while (chunk = file.read(@chunk_size))
102
+ work_items << ChunkWork.new(chunk, start_pos, chunk.length)
103
+ start_pos += chunk.length
104
+ end
105
+ end
106
+
107
+ supervisor.add_work_items(work_items)
108
+ end
109
+
110
+ def finalize_hash(results_aggregator)
111
+ return nil if results_aggregator.results.empty?
112
+
113
+ # Sort results by start position
114
+ sorted_results = results_aggregator.results.sort_by { |result| result.result[:start] }
115
+
116
+ # Concatenate all hashes with newlines
117
+ combined_hash_string = sorted_results.map { |result| result.result[:hash] }.join("\n")
118
+
119
+ # Calculate final SHA-256 hash (instead of SHA3)
120
+ Digest::SHA256.hexdigest(combined_hash_string)
121
+ end
122
+ end
123
+ end
124
+
125
+ # Example usage
126
+ if __FILE__ == $PROGRAM_NAME
127
+ if ARGV.empty?
128
+ puts "Usage: ruby hierarchical_hasher.rb <file_path> [worker_count]"
129
+ exit 1
130
+ end
131
+
132
+ file_path = ARGV[0]
133
+ worker_count = (ARGV[1] || 4).to_i
134
+
135
+ unless File.exist?(file_path)
136
+ puts "Error: File '#{file_path}' not found"
137
+ exit 1
138
+ end
139
+
140
+ puts "Starting hierarchical hasher with #{worker_count} workers..."
141
+ puts "Processing file: #{file_path}"
142
+
143
+ start_time = Time.now
144
+ hasher = HierarchicalHasher::FileHasher.new(file_path, 1024, worker_count)
145
+ final_hash = hasher.hash_file
146
+ end_time = Time.now
147
+
148
+ puts "Final SHA-256 hash: #{final_hash}"
149
+ puts "Processing completed in #{end_time - start_time} seconds"
150
+ end
@@ -0,0 +1,45 @@
1
+ = Multi-Work Type Example
2
+
3
+ == Overview
4
+
5
+ This example demonstrates how to handle multiple types of work items within a single Fractor supervisor. It shows how a single worker can process different work types intelligently, applying different strategies based on the work's type.
6
+
7
+ == Key Concepts
8
+
9
+ * *Multiple Work Types*: Supporting different work classes within the same system
10
+ * *Polymorphic Processing*: Workers that adapt their processing based on work type
11
+ * *Type Detection*: Identifying and handling different work types appropriately
12
+ * *Unified Workflow*: Managing diverse work through a common supervisor
13
+
14
+ == Example Explanation
15
+
16
+ This example implements a system that processes two distinct work types:
17
+
18
+ 1. *TextWork*: Handles text in various formats (plain text, Markdown, HTML, JSON)
19
+ 2. *ImageWork*: Processes image data with different dimensions and formats
20
+
21
+ A single worker type (`MultiFormatWorker`) is capable of handling both work types, adapting its processing strategies based on the work's class.
22
+
23
+ == Features Demonstrated
24
+
25
+ * Creating and using multiple work type classes
26
+ * Designing workers that can handle diverse work types
27
+ * Type-based processing logic
28
+ * Proper error handling across different work types
29
+ * Classification and reporting of heterogeneous results
30
+
31
+ == Running the Example
32
+
33
+ [source,sh]
34
+ ----
35
+ ruby examples/multi_work_type/multi_work_type.rb
36
+ ----
37
+
38
+ == Expected Output
39
+
40
+ The example will show:
41
+ * Processing of multiple work types
42
+ * Different processing strategies applied to each type
43
+ * Type-specific result formats
44
+ * Performance statistics for each work type
45
+ * Aggregated results organized by type
@@ -0,0 +1,319 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../../lib/fractor"
4
+
5
+ module MultiWorkType
6
+ # First work type for text data
7
+ class TextWork < Fractor::Work
8
+ def initialize(data, format = :plain, options = {})
9
+ super({ data: data, format: format, options: options })
10
+ end
11
+
12
+ def data
13
+ input[:data]
14
+ end
15
+
16
+ def format
17
+ input[:format]
18
+ end
19
+
20
+ def options
21
+ input[:options]
22
+ end
23
+
24
+ def to_s
25
+ "TextWork: format=#{format}, options=#{options}, data=#{data.to_s[0..30]}..."
26
+ end
27
+ end
28
+
29
+ # Second work type for image data
30
+ class ImageWork < Fractor::Work
31
+ def initialize(data, dimensions = [0, 0], format = :png)
32
+ super({ data: data, dimensions: dimensions, format: format })
33
+ end
34
+
35
+ def data
36
+ input[:data]
37
+ end
38
+
39
+ def dimensions
40
+ input[:dimensions]
41
+ end
42
+
43
+ def format
44
+ input[:format]
45
+ end
46
+
47
+ def to_s
48
+ "ImageWork: dimensions=#{dimensions.join("x")}, format=#{format}"
49
+ end
50
+ end
51
+
52
+ # A single worker that can process both work types
53
+ class MultiFormatWorker < Fractor::Worker
54
+ def process(work)
55
+ # Differentiate processing based on work class
56
+ if work.is_a?(TextWork)
57
+ process_text(work)
58
+ elsif work.is_a?(ImageWork)
59
+ process_image(work)
60
+ else
61
+ # Return error for unsupported work types
62
+ error = TypeError.new("Unsupported work type: #{work.class}")
63
+ Fractor::WorkResult.new(
64
+ error: error,
65
+ work: work
66
+ )
67
+ end
68
+ end
69
+
70
+ private
71
+
72
+ def process_text(work)
73
+ # Process text based on format
74
+ sleep(rand(0.01..0.05)) # Simulate processing time
75
+
76
+ processed_text = case work.format
77
+ when :markdown then process_markdown(work.data, work.options)
78
+ when :html then process_html(work.data, work.options)
79
+ when :json then process_json(work.data, work.options)
80
+ else work.data.upcase # Simple transformation for plain text
81
+ end
82
+
83
+ Fractor::WorkResult.new(
84
+ result: {
85
+ work_type: :text,
86
+ original_format: work.format,
87
+ transformed_data: processed_text,
88
+ metadata: {
89
+ word_count: processed_text.split(/\s+/).size,
90
+ char_count: processed_text.length
91
+ }
92
+ },
93
+ work: work
94
+ )
95
+ end
96
+
97
+ def process_image(work)
98
+ # Simulate image processing operations
99
+ sleep(rand(0.03..0.1)) # Simulate processing time
100
+
101
+ # Creating a safe copy of the data to avoid memory issues
102
+ # Avoid calling methods directly on the input that might cause memory issues
103
+ input_size = work.data.is_a?(String) ? work.data.size : 0
104
+
105
+ # In a real implementation, this would use image processing libraries
106
+ simulated_result = {
107
+ work_type: :image,
108
+ dimensions: work.dimensions,
109
+ format: work.format,
110
+ applied_filters: %i[sharpen contrast],
111
+ processing_metadata: {
112
+ original_size: input_size,
113
+ processed_size: (input_size * 0.8).to_i # Simulate compression
114
+ }
115
+ }
116
+
117
+ Fractor::WorkResult.new(
118
+ result: simulated_result,
119
+ work: work
120
+ )
121
+ end
122
+
123
+ # Format-specific processing methods
124
+ def process_markdown(text, _options)
125
+ # Simulate Markdown processing
126
+ headers = text.scan(/^#+\s+(.+)$/).flatten
127
+ links = text.scan(/\[(.+?)\]\((.+?)\)/)
128
+
129
+ "Processed Markdown: #{text.length} chars, #{headers.size} headers, #{links.size} links\n" \
130
+ "Headers: #{headers.join(", ")}\n" \
131
+ "#{text.gsub(/^#+\s+(.+)$/, '💫 \1 💫')}"
132
+ end
133
+
134
+ def process_html(text, _options)
135
+ # Simulate HTML processing
136
+ tags = text.scan(/<(\w+)[^>]*>/).flatten
137
+
138
+ "Processed HTML: #{text.length} chars, #{tags.size} tags\n" \
139
+ "Tags: #{tags.uniq.join(", ")}\n" \
140
+ "#{text.gsub(%r{<(\w+)[^>]*>(.+?)</\1>}, '✨\2✨')}"
141
+ end
142
+
143
+ def process_json(text, _options)
144
+ # Simulate JSON processing
145
+
146
+ data = text.nil? ? {} : eval(text) # WARNING: Using eval for demonstration only
147
+ keys = data.keys
148
+
149
+ "Processed JSON: #{keys.size} top-level keys\n" \
150
+ "Keys: #{keys.join(", ")}\n" \
151
+ "Pretty-printed: #{data}"
152
+ rescue StandardError => e
153
+ "Invalid JSON: #{e.message}"
154
+ end
155
+ end
156
+
157
+ # Controller class for the example
158
+ class ContentProcessor
159
+ attr_reader :results
160
+
161
+ def initialize(worker_count = 4)
162
+ # Create supervisor with a MultiFormatWorker pool
163
+ @supervisor = Fractor::Supervisor.new(
164
+ worker_pools: [
165
+ { worker_class: MultiFormatWorker, num_workers: worker_count }
166
+ ]
167
+ )
168
+
169
+ @results = {
170
+ text: [],
171
+ image: [],
172
+ errors: []
173
+ }
174
+ end
175
+
176
+ def process_mixed_content(text_items, image_items)
177
+ # Create TextWork objects and add them to the supervisor
178
+ text_works = text_items.map do |item|
179
+ TextWork.new(item[:data], item[:format], item[:options] || {})
180
+ end
181
+ @supervisor.add_work_items(text_works)
182
+
183
+ # Create ImageWork objects and add them to the supervisor
184
+ image_works = image_items.map do |item|
185
+ ImageWork.new(item[:data], item[:dimensions], item[:format] || :png)
186
+ end
187
+ @supervisor.add_work_items(image_works)
188
+
189
+ # Process all work
190
+ @supervisor.run
191
+
192
+ # Separate results by work type
193
+ classify_results(@supervisor.results)
194
+
195
+ # Return the statistics
196
+ {
197
+ total_items: text_items.size + image_items.size,
198
+ processed: {
199
+ text: @results[:text].size,
200
+ image: @results[:image].size
201
+ },
202
+ errors: @results[:errors].size,
203
+ results: @results
204
+ }
205
+ end
206
+
207
+ private
208
+
209
+ def classify_results(results_aggregator)
210
+ # Group results by work type
211
+ results_aggregator.results.each do |result|
212
+ if result.work.is_a?(TextWork)
213
+ @results[:text] << result.result
214
+ elsif result.work.is_a?(ImageWork)
215
+ @results[:image] << result.result
216
+ end
217
+ end
218
+
219
+ # Record errors
220
+ results_aggregator.errors.each do |error_result|
221
+ @results[:errors] << {
222
+ error: error_result.error,
223
+ work_type: error_result.work.class.name
224
+ }
225
+ end
226
+
227
+ puts "Processed #{@results[:text].size} text items and #{@results[:image].size} image items"
228
+ puts "Encountered #{@results[:errors].size} errors"
229
+ end
230
+ end
231
+ end
232
+
233
+ # Example usage
234
+ if __FILE__ == $PROGRAM_NAME
235
+ puts "Starting Multi-Work Type Processing Example"
236
+ puts "=========================================="
237
+ puts "This example demonstrates processing different types of work items:"
238
+ puts "1. Text documents in various formats (plain, markdown, HTML, JSON)"
239
+ puts "2. Image data with different formats and dimensions"
240
+ puts "Both are processed by the same worker but with different strategies"
241
+ puts
242
+
243
+ # Sample text items
244
+ text_items = [
245
+ {
246
+ data: "This is a plain text document. It has no special formatting.",
247
+ format: :plain
248
+ },
249
+ {
250
+ data: "# Markdown Document\n\nThis is a **bold** statement. Here's a [link](https://example.com).",
251
+ format: :markdown
252
+ },
253
+ {
254
+ data: "<html><body><h1>HTML Document</h1><p>This is a paragraph.</p></body></html>",
255
+ format: :html
256
+ },
257
+ {
258
+ data: "{name: 'Product', price: 29.99, tags: ['electronics', 'gadget']}",
259
+ format: :json,
260
+ options: { pretty: true }
261
+ }
262
+ ]
263
+
264
+ # Sample image items (simulated)
265
+ image_items = [
266
+ {
267
+ data: "simulated_jpeg_data_1",
268
+ dimensions: [800, 600],
269
+ format: :jpeg
270
+ },
271
+ {
272
+ data: "simulated_png_data_1",
273
+ dimensions: [1024, 768],
274
+ format: :png
275
+ },
276
+ {
277
+ data: "simulated_gif_data_1",
278
+ dimensions: [320, 240],
279
+ format: :gif
280
+ }
281
+ ]
282
+
283
+ worker_count = 4
284
+ puts "Processing with #{worker_count} workers..."
285
+ puts
286
+
287
+ start_time = Time.now
288
+ processor = MultiWorkType::ContentProcessor.new(worker_count)
289
+ result = processor.process_mixed_content(text_items, image_items)
290
+ end_time = Time.now
291
+
292
+ puts "Processing Results:"
293
+ puts "-----------------"
294
+ puts "Total items: #{result[:total_items]}"
295
+ puts "Processed text items: #{result[:processed][:text]}"
296
+ puts "Processed image items: #{result[:processed][:image]}"
297
+ puts "Errors: #{result[:errors]}"
298
+ puts
299
+
300
+ puts "Text Processing Results:"
301
+ result[:results][:text].each_with_index do |text_result, index|
302
+ puts "Text Item #{index + 1} (#{text_result[:original_format]}):"
303
+ puts " #{text_result[:transformed_data].to_s.split("\n").first}"
304
+ puts " Word count: #{text_result[:metadata][:word_count]}"
305
+ puts " Character count: #{text_result[:metadata][:char_count]}"
306
+ puts
307
+ end
308
+
309
+ puts "Image Processing Results:"
310
+ result[:results][:image].each_with_index do |image_result, index|
311
+ puts "Image Item #{index + 1} (#{image_result[:format]}):"
312
+ puts " Dimensions: #{image_result[:dimensions].join("x")}"
313
+ puts " Applied filters: #{image_result[:applied_filters].join(", ")}"
314
+ puts " Compression: #{(1 - image_result[:processing_metadata][:processed_size].to_f / image_result[:processing_metadata][:original_size]).round(2) * 100}%"
315
+ puts
316
+ end
317
+
318
+ puts "Processing completed in #{end_time - start_time} seconds"
319
+ end
@@ -0,0 +1,44 @@
1
+ = Pipeline Processing Example
2
+
3
+ == Overview
4
+
5
+ This example demonstrates the Pipeline Processing pattern implemented with Fractor. In this pattern, data flows through a series of sequential processing stages, where the output of one stage becomes the input to the next.
6
+
7
+ == Key Concepts
8
+
9
+ * *Pipeline*: A series of connected processing stages
10
+ * *Data Flow*: Information passes through each stage in sequence
11
+ * *Transformation*: Each stage performs a specific operation on the data
12
+ * *Concurrency*: Multiple items can be at different stages of the pipeline simultaneously
13
+
14
+ == Example Explanation
15
+
16
+ This example processes data through a multi-stage pipeline:
17
+
18
+ 1. *Input Stage*: Raw data is prepared for processing
19
+ 2. *Processing Stages*: Data moves through a series of transformations
20
+ 3. *Output Stage*: Final results are collected and reported
21
+
22
+ Each stage of the pipeline can run concurrently on different workers, allowing for efficient parallel processing while maintaining the required order of operations.
23
+
24
+ == Features Demonstrated
25
+
26
+ * Sequential processing with dependencies between stages
27
+ * Concurrent execution of pipeline stages
28
+ * Processing optimizations through specialized workers
29
+ * Handling data flow between processing stages
30
+
31
+ == Running the Example
32
+
33
+ [source,sh]
34
+ ----
35
+ ruby examples/pipeline_processing/pipeline_processing.rb
36
+ ----
37
+
38
+ == Expected Output
39
+
40
+ The example will show:
41
+ * Data moving through each stage of the pipeline
42
+ * Workers processing different stages concurrently
43
+ * The transformation of data at each stage
44
+ * Final results after passing through the complete pipeline