fractor 0.1.6 → 0.1.7

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.
Files changed (172) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop_todo.yml +227 -102
  3. data/README.adoc +113 -1940
  4. data/docs/.lycheeignore +16 -0
  5. data/docs/Gemfile +24 -0
  6. data/docs/README.md +157 -0
  7. data/docs/_config.yml +151 -0
  8. data/docs/_features/error-handling.adoc +1192 -0
  9. data/docs/_features/index.adoc +80 -0
  10. data/docs/_features/monitoring.adoc +589 -0
  11. data/docs/_features/signal-handling.adoc +202 -0
  12. data/docs/_features/workflows.adoc +1235 -0
  13. data/docs/_guides/continuous-mode.adoc +736 -0
  14. data/docs/_guides/cookbook.adoc +1133 -0
  15. data/docs/_guides/index.adoc +55 -0
  16. data/docs/_guides/pipeline-mode.adoc +730 -0
  17. data/docs/_guides/troubleshooting.adoc +358 -0
  18. data/docs/_pages/architecture.adoc +1390 -0
  19. data/docs/_pages/core-concepts.adoc +1392 -0
  20. data/docs/_pages/design-principles.adoc +862 -0
  21. data/docs/_pages/getting-started.adoc +290 -0
  22. data/docs/_pages/installation.adoc +143 -0
  23. data/docs/_reference/api.adoc +1080 -0
  24. data/docs/_reference/error-reporting.adoc +670 -0
  25. data/docs/_reference/examples.adoc +181 -0
  26. data/docs/_reference/index.adoc +96 -0
  27. data/docs/_reference/troubleshooting.adoc +862 -0
  28. data/docs/_tutorials/complex-workflows.adoc +1022 -0
  29. data/docs/_tutorials/data-processing-pipeline.adoc +740 -0
  30. data/docs/_tutorials/first-application.adoc +384 -0
  31. data/docs/_tutorials/index.adoc +48 -0
  32. data/docs/_tutorials/long-running-services.adoc +931 -0
  33. data/docs/assets/images/favicon-16.png +0 -0
  34. data/docs/assets/images/favicon-32.png +0 -0
  35. data/docs/assets/images/favicon-48.png +0 -0
  36. data/docs/assets/images/favicon.ico +0 -0
  37. data/docs/assets/images/favicon.png +0 -0
  38. data/docs/assets/images/favicon.svg +45 -0
  39. data/docs/assets/images/fractor-icon.svg +49 -0
  40. data/docs/assets/images/fractor-logo.svg +61 -0
  41. data/docs/index.adoc +131 -0
  42. data/docs/lychee.toml +39 -0
  43. data/examples/api_aggregator/README.adoc +627 -0
  44. data/examples/api_aggregator/api_aggregator.rb +376 -0
  45. data/examples/auto_detection/README.adoc +407 -29
  46. data/examples/continuous_chat_common/message_protocol.rb +1 -1
  47. data/examples/error_reporting.rb +207 -0
  48. data/examples/file_processor/README.adoc +170 -0
  49. data/examples/file_processor/file_processor.rb +615 -0
  50. data/examples/file_processor/sample_files/invalid.csv +1 -0
  51. data/examples/file_processor/sample_files/orders.xml +24 -0
  52. data/examples/file_processor/sample_files/products.json +23 -0
  53. data/examples/file_processor/sample_files/users.csv +6 -0
  54. data/examples/hierarchical_hasher/README.adoc +629 -41
  55. data/examples/image_processor/README.adoc +610 -0
  56. data/examples/image_processor/image_processor.rb +349 -0
  57. data/examples/image_processor/processed_images/sample_10_processed.jpg.json +12 -0
  58. data/examples/image_processor/processed_images/sample_1_processed.jpg.json +12 -0
  59. data/examples/image_processor/processed_images/sample_2_processed.jpg.json +12 -0
  60. data/examples/image_processor/processed_images/sample_3_processed.jpg.json +12 -0
  61. data/examples/image_processor/processed_images/sample_4_processed.jpg.json +12 -0
  62. data/examples/image_processor/processed_images/sample_5_processed.jpg.json +12 -0
  63. data/examples/image_processor/processed_images/sample_6_processed.jpg.json +12 -0
  64. data/examples/image_processor/processed_images/sample_7_processed.jpg.json +12 -0
  65. data/examples/image_processor/processed_images/sample_8_processed.jpg.json +12 -0
  66. data/examples/image_processor/processed_images/sample_9_processed.jpg.json +12 -0
  67. data/examples/image_processor/test_images/sample_1.png +1 -0
  68. data/examples/image_processor/test_images/sample_10.png +1 -0
  69. data/examples/image_processor/test_images/sample_2.png +1 -0
  70. data/examples/image_processor/test_images/sample_3.png +1 -0
  71. data/examples/image_processor/test_images/sample_4.png +1 -0
  72. data/examples/image_processor/test_images/sample_5.png +1 -0
  73. data/examples/image_processor/test_images/sample_6.png +1 -0
  74. data/examples/image_processor/test_images/sample_7.png +1 -0
  75. data/examples/image_processor/test_images/sample_8.png +1 -0
  76. data/examples/image_processor/test_images/sample_9.png +1 -0
  77. data/examples/log_analyzer/README.adoc +662 -0
  78. data/examples/log_analyzer/log_analyzer.rb +579 -0
  79. data/examples/log_analyzer/sample_logs/apache.log +20 -0
  80. data/examples/log_analyzer/sample_logs/json.log +15 -0
  81. data/examples/log_analyzer/sample_logs/nginx.log +15 -0
  82. data/examples/log_analyzer/sample_logs/rails.log +29 -0
  83. data/examples/multi_work_type/README.adoc +576 -26
  84. data/examples/performance_monitoring.rb +120 -0
  85. data/examples/pipeline_processing/README.adoc +740 -26
  86. data/examples/pipeline_processing/pipeline_processing.rb +2 -2
  87. data/examples/priority_work_example.rb +155 -0
  88. data/examples/producer_subscriber/README.adoc +889 -46
  89. data/examples/scatter_gather/README.adoc +829 -27
  90. data/examples/simple/README.adoc +347 -0
  91. data/examples/specialized_workers/README.adoc +622 -26
  92. data/examples/specialized_workers/specialized_workers.rb +44 -8
  93. data/examples/stream_processor/README.adoc +206 -0
  94. data/examples/stream_processor/stream_processor.rb +284 -0
  95. data/examples/web_scraper/README.adoc +625 -0
  96. data/examples/web_scraper/web_scraper.rb +285 -0
  97. data/examples/workflow/README.adoc +406 -0
  98. data/examples/workflow/circuit_breaker/README.adoc +360 -0
  99. data/examples/workflow/circuit_breaker/circuit_breaker_workflow.rb +225 -0
  100. data/examples/workflow/conditional/README.adoc +483 -0
  101. data/examples/workflow/conditional/conditional_workflow.rb +215 -0
  102. data/examples/workflow/dead_letter_queue/README.adoc +374 -0
  103. data/examples/workflow/dead_letter_queue/dead_letter_queue_workflow.rb +217 -0
  104. data/examples/workflow/fan_out/README.adoc +381 -0
  105. data/examples/workflow/fan_out/fan_out_workflow.rb +202 -0
  106. data/examples/workflow/retry/README.adoc +248 -0
  107. data/examples/workflow/retry/retry_workflow.rb +195 -0
  108. data/examples/workflow/simple_linear/README.adoc +267 -0
  109. data/examples/workflow/simple_linear/simple_linear_workflow.rb +175 -0
  110. data/examples/workflow/simplified/README.adoc +329 -0
  111. data/examples/workflow/simplified/simplified_workflow.rb +222 -0
  112. data/exe/fractor +10 -0
  113. data/lib/fractor/cli.rb +288 -0
  114. data/lib/fractor/configuration.rb +307 -0
  115. data/lib/fractor/continuous_server.rb +60 -65
  116. data/lib/fractor/error_formatter.rb +72 -0
  117. data/lib/fractor/error_report_generator.rb +152 -0
  118. data/lib/fractor/error_reporter.rb +244 -0
  119. data/lib/fractor/error_statistics.rb +147 -0
  120. data/lib/fractor/execution_tracer.rb +162 -0
  121. data/lib/fractor/logger.rb +230 -0
  122. data/lib/fractor/main_loop_handler.rb +406 -0
  123. data/lib/fractor/main_loop_handler3.rb +135 -0
  124. data/lib/fractor/main_loop_handler4.rb +299 -0
  125. data/lib/fractor/performance_metrics_collector.rb +181 -0
  126. data/lib/fractor/performance_monitor.rb +215 -0
  127. data/lib/fractor/performance_report_generator.rb +202 -0
  128. data/lib/fractor/priority_work.rb +93 -0
  129. data/lib/fractor/priority_work_queue.rb +189 -0
  130. data/lib/fractor/result_aggregator.rb +32 -0
  131. data/lib/fractor/shutdown_handler.rb +168 -0
  132. data/lib/fractor/signal_handler.rb +80 -0
  133. data/lib/fractor/supervisor.rb +382 -269
  134. data/lib/fractor/supervisor_logger.rb +88 -0
  135. data/lib/fractor/version.rb +1 -1
  136. data/lib/fractor/work.rb +12 -0
  137. data/lib/fractor/work_distribution_manager.rb +151 -0
  138. data/lib/fractor/work_queue.rb +20 -0
  139. data/lib/fractor/work_result.rb +181 -9
  140. data/lib/fractor/worker.rb +73 -0
  141. data/lib/fractor/workflow/builder.rb +210 -0
  142. data/lib/fractor/workflow/chain_builder.rb +169 -0
  143. data/lib/fractor/workflow/circuit_breaker.rb +183 -0
  144. data/lib/fractor/workflow/circuit_breaker_orchestrator.rb +208 -0
  145. data/lib/fractor/workflow/circuit_breaker_registry.rb +112 -0
  146. data/lib/fractor/workflow/dead_letter_queue.rb +334 -0
  147. data/lib/fractor/workflow/execution_hooks.rb +39 -0
  148. data/lib/fractor/workflow/execution_strategy.rb +225 -0
  149. data/lib/fractor/workflow/execution_trace.rb +134 -0
  150. data/lib/fractor/workflow/helpers.rb +191 -0
  151. data/lib/fractor/workflow/job.rb +290 -0
  152. data/lib/fractor/workflow/job_dependency_validator.rb +120 -0
  153. data/lib/fractor/workflow/logger.rb +110 -0
  154. data/lib/fractor/workflow/pre_execution_context.rb +193 -0
  155. data/lib/fractor/workflow/retry_config.rb +156 -0
  156. data/lib/fractor/workflow/retry_orchestrator.rb +184 -0
  157. data/lib/fractor/workflow/retry_strategy.rb +93 -0
  158. data/lib/fractor/workflow/structured_logger.rb +30 -0
  159. data/lib/fractor/workflow/type_compatibility_validator.rb +222 -0
  160. data/lib/fractor/workflow/visualizer.rb +211 -0
  161. data/lib/fractor/workflow/workflow_context.rb +132 -0
  162. data/lib/fractor/workflow/workflow_executor.rb +669 -0
  163. data/lib/fractor/workflow/workflow_result.rb +55 -0
  164. data/lib/fractor/workflow/workflow_validator.rb +295 -0
  165. data/lib/fractor/workflow.rb +333 -0
  166. data/lib/fractor/wrapped_ractor.rb +66 -101
  167. data/lib/fractor/wrapped_ractor3.rb +161 -0
  168. data/lib/fractor/wrapped_ractor4.rb +242 -0
  169. data/lib/fractor.rb +92 -4
  170. metadata +179 -6
  171. data/tests/sample.rb.bak +0 -309
  172. data/tests/sample_working.rb.bak +0 -209
@@ -0,0 +1,88 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Fractor
4
+ # Handles logging for Supervisor operations.
5
+ # Extracted from Supervisor to follow Single Responsibility Principle.
6
+ class SupervisorLogger
7
+ attr_reader :logger, :debug_enabled
8
+
9
+ def initialize(logger: :default, debug: false)
10
+ @logger = if logger == :default
11
+ Fractor.logger
12
+ else
13
+ logger
14
+ end
15
+ @debug_enabled = debug
16
+ end
17
+
18
+ # Log debug message (only when debug mode is enabled)
19
+ def debug(message)
20
+ return unless @debug_enabled
21
+
22
+ if @logger
23
+ @logger.debug("[Fractor] #{message}")
24
+ else
25
+ puts "[DEBUG] #{message}"
26
+ end
27
+ end
28
+
29
+ # Log info message
30
+ def info(message)
31
+ if @logger
32
+ @logger.info("[Fractor] #{message}")
33
+ else
34
+ puts "[INFO] #{message}"
35
+ end
36
+ end
37
+
38
+ # Log warning message
39
+ def warn(message)
40
+ if @logger
41
+ @logger.warn("[Fractor] #{message}")
42
+ else
43
+ Kernel.warn "[WARN] #{message}"
44
+ end
45
+ end
46
+
47
+ # Log error message
48
+ def error(message)
49
+ if @logger
50
+ @logger.error("[Fractor] #{message}")
51
+ else
52
+ Kernel.warn "[ERROR] #{message}"
53
+ end
54
+ end
55
+
56
+ # Log work item status
57
+ def log_work_added(work, total_count, queue_size)
58
+ debug "Work item added: #{work.inspect}"
59
+ debug "Initial work count: #{total_count}, Queue size: #{queue_size}"
60
+ end
61
+
62
+ # Log worker status
63
+ def log_worker_status(total:, idle:, busy:)
64
+ debug "Workers: #{total} total, #{idle} idle, #{busy} busy"
65
+ end
66
+
67
+ # Log processing status
68
+ def log_processing_status(processed:, total:, queue_size:)
69
+ debug "Processing: #{processed}/#{total}, Queue size: #{queue_size}"
70
+ end
71
+
72
+ # Log result received
73
+ def log_result_received(result)
74
+ debug "Result received: #{result.inspect}"
75
+ end
76
+
77
+ # Log error received
78
+ def log_error_received(error_result)
79
+ error "Error in worker: #{error_result.error}"
80
+ error "Work item: #{error_result.work.inspect}"
81
+ end
82
+
83
+ # Enable or disable debug mode
84
+ def debug=(enabled)
85
+ @debug_enabled = enabled
86
+ end
87
+ end
88
+ end
@@ -2,5 +2,5 @@
2
2
 
3
3
  module Fractor
4
4
  # Fractor version
5
- VERSION = "0.1.6"
5
+ VERSION = "0.1.7"
6
6
  end
data/lib/fractor/work.rb CHANGED
@@ -13,5 +13,17 @@ module Fractor
13
13
  def to_s
14
14
  "Work: #{@input}"
15
15
  end
16
+
17
+ # Provide detailed inspection of work item for debugging
18
+ # @return [String] Detailed inspection string
19
+ def inspect
20
+ details = [
21
+ "#<#{self.class.name}",
22
+ "0x#{(object_id << 1).to_s(16)}",
23
+ "@input=#{@input.inspect}",
24
+ "@type=#{input.class.name}",
25
+ ]
26
+ details.join(" ")
27
+ end
16
28
  end
17
29
  end
@@ -0,0 +1,151 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Fractor
4
+ # Manages work distribution to workers in a Supervisor.
5
+ # Responsible for tracking idle workers and assigning work from the queue.
6
+ #
7
+ # This class extracts work distribution logic from Supervisor to follow
8
+ # the Single Responsibility Principle.
9
+ class WorkDistributionManager
10
+ attr_reader :idle_workers
11
+
12
+ def initialize(work_queue, workers, ractors_map, debug: false,
13
+ continuous_mode: false, performance_monitor: nil)
14
+ @work_queue = work_queue
15
+ @workers = workers
16
+ @ractors_map = ractors_map
17
+ @debug = debug
18
+ @continuous_mode = continuous_mode
19
+ @performance_monitor = performance_monitor
20
+ @idle_workers = []
21
+ @work_start_times = {}
22
+ end
23
+
24
+ # Update the workers reference after workers are created
25
+ # This is needed when @workers is reassigned in Supervisor.start_workers
26
+ #
27
+ # @param workers [Array<WrappedRactor>] The new workers array
28
+ def update_workers(workers)
29
+ @workers = workers
30
+ end
31
+
32
+ # Assign work to a specific worker if work is available.
33
+ #
34
+ # @param wrapped_ractor [WrappedRactor] The worker to assign work to
35
+ # @return [Boolean] true if work was sent, false otherwise
36
+ def assign_work_to_worker(wrapped_ractor)
37
+ # Ensure the wrapped_ractor instance is valid and its underlying ractor is not closed
38
+ if wrapped_ractor && !wrapped_ractor.closed?
39
+ if @work_queue.empty?
40
+ puts "Work queue empty. Not sending new work to Ractor #{wrapped_ractor.name}." if @debug
41
+ false
42
+ else
43
+ work_item = @work_queue.pop # Now directly a Work object
44
+
45
+ # Track start time for performance monitoring
46
+ if @performance_monitor
47
+ @work_start_times[work_item.object_id] = Time.now
48
+ end
49
+
50
+ puts "Sending next work #{work_item.inspect} to Ractor: #{wrapped_ractor.name}." if @debug
51
+ wrapped_ractor.send(work_item) # Send the Work object
52
+ puts "Work sent to #{wrapped_ractor.name}." if @debug
53
+
54
+ # Remove from idle workers list since it's now busy
55
+ @idle_workers.delete(wrapped_ractor)
56
+ true
57
+ end
58
+ else
59
+ puts "Attempted to send work to an invalid or closed Ractor: #{wrapped_ractor&.name || 'unknown'}." if @debug
60
+ # Remove from map if found but closed
61
+ if wrapped_ractor && @ractors_map.key?(wrapped_ractor.ractor)
62
+ @ractors_map.delete(wrapped_ractor.ractor)
63
+ end
64
+ false
65
+ end
66
+ end
67
+
68
+ # Mark a worker as idle (available for work).
69
+ #
70
+ # @param wrapped_ractor [WrappedRactor] The worker to mark as idle
71
+ def mark_worker_idle(wrapped_ractor)
72
+ @idle_workers << wrapped_ractor unless @idle_workers.include?(wrapped_ractor)
73
+ puts "Worker #{wrapped_ractor.name} marked as idle." if @debug
74
+ end
75
+
76
+ # Mark a worker as busy (not available for work).
77
+ #
78
+ # @param wrapped_ractor [WrappedRactor] The worker to mark as busy
79
+ def mark_worker_busy(wrapped_ractor)
80
+ @idle_workers.delete(wrapped_ractor)
81
+ end
82
+
83
+ # Distribute available work to idle workers.
84
+ # Useful when new work is added to the queue.
85
+ #
86
+ # @return [Integer] Number of workers that received work
87
+ def distribute_to_idle_workers
88
+ distributed = 0
89
+ while !@work_queue.empty? && !@idle_workers.empty?
90
+ worker = @idle_workers.shift
91
+ if assign_work_to_worker(worker)
92
+ puts "Sent work to idle worker #{worker.name}" if @debug
93
+ distributed += 1
94
+ end
95
+ end
96
+ distributed
97
+ end
98
+
99
+ # Get the list of idle (available) workers.
100
+ #
101
+ # @return [Array<WrappedRactor>] List of idle workers
102
+ def idle_workers_list
103
+ @idle_workers.dup
104
+ end
105
+
106
+ # Get the list of busy (processing) workers.
107
+ #
108
+ # @return [Array<WrappedRactor>] List of busy workers
109
+ def busy_workers_list
110
+ @workers.reject { |w| @idle_workers.include?(w) }
111
+ end
112
+
113
+ # Get the count of idle workers.
114
+ #
115
+ # @return [Integer] Number of idle workers
116
+ def idle_count
117
+ @idle_workers.size
118
+ end
119
+
120
+ # Get the count of busy workers.
121
+ #
122
+ # @return [Integer] Number of busy workers
123
+ def busy_count
124
+ @workers.size - @idle_workers.size
125
+ end
126
+
127
+ # Get the work start time for a specific work item.
128
+ #
129
+ # @param work_object_id [Integer] The object_id of the work item
130
+ # @return [Time, nil] The start time, or nil if not found
131
+ def get_work_start_time(work_object_id)
132
+ @work_start_times.delete(work_object_id)
133
+ end
134
+
135
+ # Clear all tracked work start times.
136
+ # Useful for cleanup or testing.
137
+ def clear_work_start_times
138
+ @work_start_times.clear
139
+ end
140
+
141
+ # Get current worker status summary.
142
+ #
143
+ # @return [Hash] Worker status summary with :idle and :busy counts
144
+ def status_summary
145
+ {
146
+ idle: @idle_workers.size,
147
+ busy: @workers.size - @idle_workers.size,
148
+ }
149
+ end
150
+ end
151
+ end
@@ -16,6 +16,13 @@ module Fractor
16
16
  # @param work_item [Fractor::Work] The work item to add
17
17
  # @return [void]
18
18
  def <<(work_item)
19
+ enqueue(work_item)
20
+ end
21
+
22
+ # Add a work item to the queue (standardized API)
23
+ # @param work_item [Fractor::Work] The work item to add
24
+ # @return [void]
25
+ def enqueue(work_item)
19
26
  unless work_item.is_a?(Fractor::Work)
20
27
  raise ArgumentError,
21
28
  "#{work_item.class} must be an instance of Fractor::Work"
@@ -28,6 +35,13 @@ module Fractor
28
35
  # @param max_items [Integer] Maximum number of items to retrieve
29
36
  # @return [Array<Fractor::Work>] Array of work items (may be empty)
30
37
  def pop_batch(max_items = 10)
38
+ dequeue_batch(max_items)
39
+ end
40
+
41
+ # Retrieve multiple work items from the queue in a single operation (standardized API)
42
+ # @param max_items [Integer] Maximum number of items to retrieve
43
+ # @return [Array<Fractor::Work>] Array of work items (may be empty)
44
+ def dequeue_batch(max_items = 10)
31
45
  items = []
32
46
  max_items.times do
33
47
  break if @queue.empty?
@@ -42,6 +56,12 @@ module Fractor
42
56
  items
43
57
  end
44
58
 
59
+ # Retrieve a single work item from the queue (blocking if empty)
60
+ # @return [Fractor::Work, nil] A work item or nil if queue is closed
61
+ def dequeue
62
+ @queue.pop
63
+ end
64
+
45
65
  # Check if the queue is empty
46
66
  # @return [Boolean] true if the queue is empty
47
67
  def empty?
@@ -2,34 +2,206 @@
2
2
 
3
3
  module Fractor
4
4
  # Represents the result of processing a Work item.
5
- # Can hold either a successful result or an error.
5
+ # Can hold either a successful result or an error with rich metadata.
6
6
  class WorkResult
7
- attr_reader :result, :error, :work
7
+ # Error severity levels
8
+ SEVERITY_CRITICAL = :critical # System-breaking errors
9
+ SEVERITY_ERROR = :error # Standard errors
10
+ SEVERITY_WARNING = :warning # Non-fatal issues
11
+ SEVERITY_INFO = :info # Informational
8
12
 
9
- def initialize(result: nil, error: nil, work: nil)
13
+ # Error categories
14
+ CATEGORY_VALIDATION = :validation # Input validation errors
15
+ CATEGORY_TIMEOUT = :timeout # Timeout errors
16
+ CATEGORY_NETWORK = :network # Network-related errors
17
+ CATEGORY_RESOURCE = :resource # Resource exhaustion
18
+ CATEGORY_BUSINESS = :business # Business logic errors
19
+ CATEGORY_SYSTEM = :system # System errors
20
+ CATEGORY_UNKNOWN = :unknown # Unknown/uncategorized
21
+
22
+ attr_reader :result, :error, :work, :error_code, :error_context,
23
+ :error_category, :error_severity, :suggestion, :stack_trace
24
+
25
+ def initialize(
26
+ result: nil,
27
+ error: nil,
28
+ work: nil,
29
+ error_code: nil,
30
+ error_context: nil,
31
+ error_category: nil,
32
+ error_severity: nil,
33
+ suggestion: nil,
34
+ stack_trace: nil
35
+ )
10
36
  @result = result
11
37
  @error = error
12
38
  @work = work
39
+ @error_code = error_code
40
+ @error_context = error_context || {}
41
+ @error_category = error_category || infer_category(error)
42
+ @error_severity = error_severity || infer_severity(error)
43
+ @suggestion = suggestion || infer_suggestion(error)
44
+ @stack_trace = stack_trace || capture_stack_trace(error)
13
45
  end
14
46
 
15
47
  def success?
16
48
  !@error
17
49
  end
18
50
 
51
+ def failure?
52
+ !success?
53
+ end
54
+
55
+ # Check if error is critical
56
+ def critical?
57
+ @error_severity == SEVERITY_CRITICAL
58
+ end
59
+
60
+ # Check if error is retriable based on category
61
+ def retriable?
62
+ return false if success?
63
+
64
+ retriable_categories = [
65
+ CATEGORY_TIMEOUT,
66
+ CATEGORY_NETWORK,
67
+ CATEGORY_RESOURCE,
68
+ ]
69
+ retriable_categories.include?(@error_category)
70
+ end
71
+
72
+ # Get full error information as hash
73
+ def error_info
74
+ return nil if success?
75
+
76
+ {
77
+ error: @error,
78
+ error_class: @error&.class&.name,
79
+ error_message: @error&.message,
80
+ error_code: @error_code,
81
+ error_category: @error_category,
82
+ error_severity: @error_severity,
83
+ error_context: @error_context,
84
+ suggestion: @suggestion,
85
+ stack_trace: @stack_trace,
86
+ }
87
+ end
88
+
19
89
  def to_s
20
90
  if success?
21
91
  "Result: #{@result}"
22
92
  else
23
- "Error: #{@error}, Work: #{@work}"
93
+ "Error: #{@error}, Code: #{@error_code}, Category: #{@error_category}, Severity: #{@error_severity}"
24
94
  end
25
95
  end
26
96
 
27
97
  def inspect
28
- {
29
- result: @result,
30
- error: @error,
31
- work: @work&.to_s, # Use safe navigation for work
32
- }
98
+ if success?
99
+ {
100
+ result: @result,
101
+ work: @work&.to_s,
102
+ }
103
+ else
104
+ {
105
+ error: @error,
106
+ error_code: @error_code,
107
+ error_category: @error_category,
108
+ error_severity: @error_severity,
109
+ error_context: @error_context,
110
+ work: @work&.to_s,
111
+ }
112
+ end
113
+ end
114
+
115
+ private
116
+
117
+ # Infer error category from error type
118
+ def infer_category(error)
119
+ return nil unless error
120
+
121
+ case error
122
+ when ArgumentError, TypeError
123
+ CATEGORY_VALIDATION
124
+ when Timeout::Error
125
+ CATEGORY_TIMEOUT
126
+ when String
127
+ # Categorize string error messages by content
128
+ error_msg = error.downcase
129
+ if error_msg.include?("timeout") || error_msg.include?("timed out")
130
+ CATEGORY_TIMEOUT
131
+ elsif error_msg.include?("connection") || error_msg.include?("network") ||
132
+ error_msg.include?("socket") || error_msg.include?("refused")
133
+ CATEGORY_NETWORK
134
+ elsif error_msg.include?("memory") || error_msg.include?("space")
135
+ CATEGORY_RESOURCE
136
+ else
137
+ CATEGORY_UNKNOWN
138
+ end
139
+ when defined?(SocketError) ? SocketError : nil, Errno::ECONNREFUSED, Errno::ETIMEDOUT
140
+ CATEGORY_NETWORK
141
+ when Errno::ENOMEM, Errno::ENOSPC
142
+ CATEGORY_RESOURCE
143
+ when SystemCallError, SystemStackError
144
+ CATEGORY_SYSTEM
145
+ else
146
+ CATEGORY_UNKNOWN
147
+ end
148
+ end
149
+
150
+ # Infer error severity from error type
151
+ def infer_severity(error)
152
+ return nil unless error
153
+
154
+ case error
155
+ when SystemStackError, Errno::ENOMEM
156
+ SEVERITY_CRITICAL
157
+ when StandardError
158
+ SEVERITY_ERROR
159
+ else
160
+ SEVERITY_WARNING
161
+ end
162
+ end
163
+
164
+ # Infer suggestion from error type
165
+ def infer_suggestion(error)
166
+ return nil unless error
167
+
168
+ error_msg = error.to_s.downcase
169
+
170
+ case error_msg
171
+ when /negative number/i, /must be positive/i
172
+ "Ensure input values are positive. Consider using absolute value or validating input range."
173
+ when /timeout/i
174
+ "Consider increasing timeout duration or breaking work into smaller chunks."
175
+ when /memory/i, /out of memory/i
176
+ "Try processing smaller batches or increasing available memory."
177
+ when /connection/i, /network/i, /refused/i
178
+ "Verify network connectivity and service availability. Check firewall settings."
179
+ when /undefined method/i, /no method/i
180
+ "Ensure the Worker class implements all required methods for the Work type."
181
+ when /nil/i, /null/i
182
+ "Check if work items are being initialized with valid input data."
183
+ when /argument/i, /type/i
184
+ "Verify input data types match expected format. Check Work item initialization."
185
+ when /file/i, /not found/i
186
+ "Ensure file paths are correct and files exist before processing."
187
+ when /permission/i, /authorized/i
188
+ "Check file permissions and ensure proper access rights for the operation."
189
+ else
190
+ "Check the error message and ensure all requirements are met. Enable debug logging for more details."
191
+ end
192
+ end
193
+
194
+ # Capture stack trace from error if available
195
+ def capture_stack_trace(error)
196
+ return nil unless error
197
+
198
+ # If error is a string, return nil
199
+ return nil unless error.is_a?(Exception)
200
+
201
+ # Get backtrace from exception
202
+ error.backtrace
203
+ rescue StandardError
204
+ nil
33
205
  end
34
206
  end
35
207
  end
@@ -1,17 +1,90 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "timeout"
4
+
3
5
  module Fractor
4
6
  # Base class for defining work processors.
5
7
  # Subclasses must implement the `process` method.
6
8
  class Worker
9
+ class << self
10
+ attr_reader :input_type_class, :output_type_class, :worker_timeout
11
+
12
+ # Declare the input type for this worker.
13
+ # Used by the workflow system to validate data flow.
14
+ #
15
+ # @param klass [Class] A Lutaml::Model::Serializable subclass
16
+ def input_type(klass)
17
+ validate_type_class!(klass, "input_type")
18
+ @input_type_class = klass
19
+ end
20
+
21
+ # Declare the output type for this worker.
22
+ # Used by the workflow system to validate data flow.
23
+ #
24
+ # @param klass [Class] A Lutaml::Model::Serializable subclass
25
+ def output_type(klass)
26
+ validate_type_class!(klass, "output_type")
27
+ @output_type_class = klass
28
+ end
29
+
30
+ # Set a timeout for this worker's process method.
31
+ # If the process method takes longer than this, a Timeout::Error will be raised.
32
+ #
33
+ # @param seconds [Numeric] Timeout in seconds
34
+ def timeout(seconds)
35
+ @worker_timeout = seconds
36
+ end
37
+
38
+ # Get the effective timeout for this worker.
39
+ # Returns the worker-specific timeout, or the global default if not set.
40
+ # Note: This method accesses Fractor.config and should be called from
41
+ # the main ractor context, not from within worker ractors.
42
+ #
43
+ # @return [Numeric, nil] Timeout in seconds, or nil if not configured
44
+ def effective_timeout
45
+ @worker_timeout || begin
46
+ # Access config safely - this must be called from main ractor
47
+ Fractor.config.default_worker_timeout
48
+ end
49
+ end
50
+
51
+ private
52
+
53
+ def validate_type_class!(klass, method_name)
54
+ # Allow any class for now, stricter validation can be added later
55
+ # In production, you'd want to check for Lutaml::Model::Serializable
56
+ return if klass.is_a?(Class)
57
+
58
+ raise ArgumentError, "#{method_name} must be a Class"
59
+ end
60
+ end
61
+
7
62
  def initialize(name: nil, **options)
8
63
  @name = name
9
64
  @options = options
65
+ # If timeout is not provided or is nil, fall back to class-level timeout
66
+ # Note: This must only be called from the main ractor, not from within worker ractors.
67
+ # In the ractor context, timeout should always be passed explicitly.
68
+ if !@options.key?(:timeout) || @options[:timeout].nil?
69
+ @options[:timeout] = self.class.worker_timeout
70
+ end
10
71
  end
11
72
 
12
73
  def process(work)
13
74
  raise NotImplementedError,
14
75
  "Subclasses must implement the 'process' method."
15
76
  end
77
+
78
+ # Get the timeout for this worker instance.
79
+ # Uses the class-level timeout if not overridden.
80
+ # Note: This method is safe to call from within ractors as it only
81
+ # accesses instance variables that were set at initialization time.
82
+ #
83
+ # @return [Numeric, nil] Timeout in seconds, or nil if not configured
84
+ def timeout
85
+ # The timeout is always set at initialization time via options,
86
+ # so we can safely access it from within the ractor
87
+ @options[:timeout]
88
+ end
16
89
  end
17
90
  end