fractor 0.1.8 → 0.1.9

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e02a928040e2df09a69eda8ffa10367d5e698868a44643912a747a99b27111fa
4
- data.tar.gz: e02247543fd89c0856ac6ce8e18c538ef32c5e1f82bce856b666b7db51b49d26
3
+ metadata.gz: 9b4ea06ce5fb4dc056cd294dcadeac07cca24610a0b995e5f910b3ef0075e0ea
4
+ data.tar.gz: 1adc8b962143e4713259b18c5545ab39ee0a62b11db5880b0389ff64449a27b8
5
5
  SHA512:
6
- metadata.gz: 8b664bc5b52225acc157a3f37a1ae97a6a621c08398d2de6b7759a03aceb1bb356d7242da173d6f16f331e0433ab29b60b067250b57422ab5d3c9bed2a284cc5
7
- data.tar.gz: 26fa65f4a034857688d3fd45f0702eed50a9c07262107e4a43b1da0d77656988137a5242c661e03ef571aa35cfb1ed11a9e48b7fa17f626e74eb5ab71111a3d8
6
+ metadata.gz: 31800cb4767762f1bdd1fec315cda164fd5effb3fd72bb327b4a30788ac41b2b2cd0ec3bcf2548e82fe69ccf97150007222e81c875bcb5f78c01d4e28162ef03
7
+ data.tar.gz: 728316725154c5fde32f2bb8fb6ab584b26814ad463af43c4928a333083ed34e6d66dc781c6edad82128644f0f6b6197c31303aa6353ec586aeaa81212dd911d
data/.rubocop_todo.yml CHANGED
@@ -1,6 +1,6 @@
1
1
  # This configuration was generated by
2
2
  # `rubocop --auto-gen-config`
3
- # on 2026-01-22 06:39:26 UTC using RuboCop version 1.82.1.
3
+ # on 2026-01-22 09:15:54 UTC using RuboCop version 1.82.1.
4
4
  # The point is for the user to remove these configuration records
5
5
  # one by one as the offenses are removed from the code base.
6
6
  # Note that changes in the inspected code, or installation of new
@@ -11,47 +11,86 @@ Gemspec/RequiredRubyVersion:
11
11
  Exclude:
12
12
  - 'fractor.gemspec'
13
13
 
14
- # Offense count: 4
14
+ # Offense count: 1
15
15
  # This cop supports safe autocorrection (--autocorrect).
16
16
  # Configuration parameters: EnforcedStyle, IndentationWidth.
17
17
  # SupportedStyles: with_first_argument, with_fixed_indentation
18
18
  Layout/ArgumentAlignment:
19
19
  Exclude:
20
- - 'spec/fractor/main_loop_handler3_spec.rb'
21
- - 'spec/fractor/main_loop_handler4_spec.rb'
22
- - 'spec/fractor/supervisor_shutdown_spec.rb'
20
+ - 'lib/fractor/queue_persister.rb'
21
+
22
+ # Offense count: 1
23
+ # This cop supports safe autocorrection (--autocorrect).
24
+ # Configuration parameters: EnforcedStyle, IndentationWidth.
25
+ # SupportedStyles: with_first_element, with_fixed_indentation
26
+ Layout/ArrayAlignment:
27
+ Exclude:
28
+ - 'lib/fractor/queue_persister.rb'
23
29
 
24
30
  # Offense count: 2
25
31
  # This cop supports safe autocorrection (--autocorrect).
26
- # Configuration parameters: EnforcedStyleAlignWith.
27
- # SupportedStylesAlignWith: either, start_of_block, start_of_line
28
- Layout/BlockAlignment:
32
+ # Configuration parameters: IndentationWidth.
33
+ Layout/AssignmentIndentation:
29
34
  Exclude:
30
- - 'spec/fractor/main_loop_handler3_spec.rb'
31
- - 'spec/fractor/main_loop_handler4_spec.rb'
35
+ - 'lib/fractor/queue_persister.rb'
36
+ - 'lib/fractor/result_cache.rb'
37
+
38
+ # Offense count: 1
39
+ # This cop supports safe autocorrection (--autocorrect).
40
+ Layout/ClosingParenthesisIndentation:
41
+ Exclude:
42
+ - 'spec/fractor/persistent_work_queue_spec.rb'
43
+
44
+ # Offense count: 1
45
+ # This cop supports safe autocorrection (--autocorrect).
46
+ # Configuration parameters: EnforcedStyle, IndentationWidth.
47
+ # SupportedStyles: consistent, consistent_relative_to_receiver, special_for_inner_method_call, special_for_inner_method_call_in_parentheses
48
+ Layout/FirstArgumentIndentation:
49
+ Exclude:
50
+ - 'spec/fractor/persistent_work_queue_spec.rb'
51
+
52
+ # Offense count: 2
53
+ # This cop supports safe autocorrection (--autocorrect).
54
+ # Configuration parameters: AllowMultipleStyles, EnforcedHashRocketStyle, EnforcedColonStyle, EnforcedLastArgumentHashStyle.
55
+ # SupportedHashRocketStyles: key, separator, table
56
+ # SupportedColonStyles: key, separator, table
57
+ # SupportedLastArgumentHashStyles: always_inspect, always_ignore, ignore_implicit, ignore_explicit
58
+ Layout/HashAlignment:
59
+ Exclude:
60
+ - 'spec/fractor/persistent_work_queue_spec.rb'
61
+ - 'spec/fractor/work_timeout_spec.rb'
32
62
 
33
- # Offense count: 455
63
+ # Offense count: 470
34
64
  # This cop supports safe autocorrection (--autocorrect).
35
65
  # Configuration parameters: Max, AllowHeredoc, AllowURI, AllowQualifiedName, URISchemes, AllowRBSInlineAnnotation, AllowCopDirectives, AllowedPatterns, SplitStrings.
36
66
  # URISchemes: http, https
37
67
  Layout/LineLength:
38
68
  Enabled: false
39
69
 
40
- # Offense count: 4
70
+ # Offense count: 1
71
+ # This cop supports safe autocorrection (--autocorrect).
72
+ # Configuration parameters: EnforcedStyle.
73
+ # SupportedStyles: symmetrical, new_line, same_line
74
+ Layout/MultilineMethodCallBraceLayout:
75
+ Exclude:
76
+ - 'spec/fractor/persistent_work_queue_spec.rb'
77
+
78
+ # Offense count: 6
41
79
  # This cop supports safe autocorrection (--autocorrect).
42
80
  # Configuration parameters: AllowInHeredoc.
43
81
  Layout/TrailingWhitespace:
44
82
  Exclude:
45
- - 'spec/fractor/main_loop_handler3_spec.rb'
46
- - 'spec/fractor/main_loop_handler4_spec.rb'
47
- - 'spec/fractor/supervisor_shutdown_spec.rb'
83
+ - 'lib/fractor/queue_persister.rb'
84
+ - 'lib/fractor/result_cache.rb'
85
+ - 'spec/fractor/persistent_work_queue_spec.rb'
86
+ - 'spec/fractor/work_timeout_spec.rb'
48
87
 
49
88
  # Offense count: 1
50
89
  Lint/BinaryOperatorWithIdenticalOperands:
51
90
  Exclude:
52
91
  - 'spec/fractor/priority_work_spec.rb'
53
92
 
54
- # Offense count: 14
93
+ # Offense count: 20
55
94
  # Configuration parameters: AllowedMethods.
56
95
  # AllowedMethods: enums
57
96
  Lint/ConstantDefinitionInBlock:
@@ -60,7 +99,10 @@ Lint/ConstantDefinitionInBlock:
60
99
  - 'spec/examples/performance_monitoring_spec.rb'
61
100
  - 'spec/examples/priority_work_example_spec.rb'
62
101
  - 'spec/fractor/integration_spec.rb'
102
+ - 'spec/fractor/persistent_work_queue_spec.rb'
103
+ - 'spec/fractor/result_cache_spec.rb'
63
104
  - 'spec/fractor/work_spec.rb'
105
+ - 'spec/fractor/work_timeout_spec.rb'
64
106
  - 'spec/fractor/worker_timeout_spec.rb'
65
107
  - 'spec/fractor/workflow/execution_strategy_spec.rb'
66
108
 
@@ -77,10 +119,11 @@ Lint/HashCompareByIdentity:
77
119
  - 'lib/fractor/work_distribution_manager.rb'
78
120
  - 'spec/fractor/work_distribution_manager_spec.rb'
79
121
 
80
- # Offense count: 1
122
+ # Offense count: 4
81
123
  # Configuration parameters: AllowedParentClasses.
82
124
  Lint/MissingSuper:
83
125
  Exclude:
126
+ - 'lib/fractor/queue_persister.rb'
84
127
  - 'spec/fractor/wrapped_ractor3_spec.rb'
85
128
 
86
129
  # Offense count: 5
@@ -91,7 +134,7 @@ Lint/RescueException:
91
134
  - 'lib/fractor/wrapped_ractor3.rb'
92
135
  - 'lib/fractor/wrapped_ractor4.rb'
93
136
 
94
- # Offense count: 4
137
+ # Offense count: 2
95
138
  # This cop supports safe autocorrection (--autocorrect).
96
139
  # Configuration parameters: AllowUnusedKeywordArguments, IgnoreEmptyMethods, IgnoreNotImplementedMethods, NotImplementedExceptions.
97
140
  # NotImplementedExceptions: NotImplementedError
@@ -99,9 +142,8 @@ Lint/UnusedMethodArgument:
99
142
  Exclude:
100
143
  - 'lib/fractor/workflow.rb'
101
144
  - 'lib/fractor/wrapped_ractor3.rb'
102
- - 'spec/fractor/wrapped_ractor3_spec.rb'
103
145
 
104
- # Offense count: 88
146
+ # Offense count: 89
105
147
  # Configuration parameters: AllowedMethods, AllowedPatterns, CountRepeatedAttributes, Max.
106
148
  Metrics/AbcSize:
107
149
  Enabled: false
@@ -110,24 +152,24 @@ Metrics/AbcSize:
110
152
  # Configuration parameters: CountComments, CountAsOne, AllowedMethods, AllowedPatterns, inherit_mode.
111
153
  # AllowedMethods: refine
112
154
  Metrics/BlockLength:
113
- Max: 105
155
+ Max: 110
114
156
 
115
- # Offense count: 61
157
+ # Offense count: 64
116
158
  # Configuration parameters: AllowedMethods, AllowedPatterns, Max.
117
159
  Metrics/CyclomaticComplexity:
118
160
  Enabled: false
119
161
 
120
- # Offense count: 142
162
+ # Offense count: 149
121
163
  # Configuration parameters: CountComments, CountAsOne, AllowedMethods, AllowedPatterns.
122
164
  Metrics/MethodLength:
123
- Max: 117
165
+ Max: 122
124
166
 
125
167
  # Offense count: 9
126
168
  # Configuration parameters: CountKeywordArgs, MaxOptionalParameters.
127
169
  Metrics/ParameterLists:
128
170
  Max: 9
129
171
 
130
- # Offense count: 46
172
+ # Offense count: 49
131
173
  # Configuration parameters: AllowedMethods, AllowedPatterns, Max.
132
174
  Metrics/PerceivedComplexity:
133
175
  Enabled: false
@@ -185,21 +227,12 @@ RSpec/ContextWording:
185
227
  - 'spec/fractor/supervisor_spec.rb'
186
228
  - 'spec/fractor_spec.rb'
187
229
 
188
- # Offense count: 27
230
+ # Offense count: 28
189
231
  # Configuration parameters: IgnoredMetadata.
190
232
  RSpec/DescribeClass:
191
233
  Enabled: false
192
234
 
193
- # Offense count: 3
194
- # This cop supports unsafe autocorrection (--autocorrect-all).
195
- # Configuration parameters: SkipBlocks, EnforcedStyle, OnlyStaticConstants.
196
- # SupportedStyles: described_class, explicit
197
- RSpec/DescribedClass:
198
- Exclude:
199
- - 'spec/fractor/worker_spec.rb'
200
- - 'spec/fractor/worker_timeout_spec.rb'
201
-
202
- # Offense count: 510
235
+ # Offense count: 542
203
236
  # Configuration parameters: CountAsOne.
204
237
  RSpec/ExampleLength:
205
238
  Max: 47
@@ -246,14 +279,17 @@ RSpec/InstanceVariable:
246
279
  - 'spec/examples/workflow/dead_letter_queue_workflow_spec.rb'
247
280
  - 'spec/fractor/integration_spec.rb'
248
281
 
249
- # Offense count: 16
282
+ # Offense count: 22
250
283
  RSpec/LeakyConstantDeclaration:
251
284
  Exclude:
252
285
  - 'spec/examples/error_reporting_spec.rb'
253
286
  - 'spec/examples/performance_monitoring_spec.rb'
254
287
  - 'spec/examples/priority_work_example_spec.rb'
255
288
  - 'spec/fractor/integration_spec.rb'
289
+ - 'spec/fractor/persistent_work_queue_spec.rb'
290
+ - 'spec/fractor/result_cache_spec.rb'
256
291
  - 'spec/fractor/work_spec.rb'
292
+ - 'spec/fractor/work_timeout_spec.rb'
257
293
  - 'spec/fractor/worker_timeout_spec.rb'
258
294
  - 'spec/fractor/workflow/execution_strategy_spec.rb'
259
295
 
@@ -265,17 +301,19 @@ RSpec/MessageSpies:
265
301
  - 'spec/fractor/main_loop_handler_spec.rb'
266
302
  - 'spec/fractor/shutdown_handler_spec.rb'
267
303
 
268
- # Offense count: 6
304
+ # Offense count: 8
269
305
  RSpec/MultipleDescribes:
270
306
  Exclude:
271
307
  - 'spec/examples/workflow/simplified_workflow_spec.rb'
308
+ - 'spec/fractor/persistent_work_queue_spec.rb'
309
+ - 'spec/fractor/work_timeout_spec.rb'
272
310
  - 'spec/fractor/worker_timeout_spec.rb'
273
311
  - 'spec/fractor/workflow/dead_letter_queue_spec.rb'
274
312
  - 'spec/fractor/workflow/execution_strategy_spec.rb'
275
313
  - 'spec/fractor/workflow/retry_config_spec.rb'
276
314
  - 'spec/fractor/workflow/retry_strategy_spec.rb'
277
315
 
278
- # Offense count: 605
316
+ # Offense count: 635
279
317
  RSpec/MultipleExpectations:
280
318
  Max: 12
281
319
 
@@ -321,9 +359,29 @@ Security/Eval:
321
359
  Exclude:
322
360
  - 'lib/fractor/cli.rb'
323
361
 
362
+ # Offense count: 1
363
+ Security/MarshalLoad:
364
+ Exclude:
365
+ - 'lib/fractor/queue_persister.rb'
366
+
367
+ # Offense count: 2
368
+ # This cop supports safe autocorrection (--autocorrect).
369
+ Style/MultilineIfModifier:
370
+ Exclude:
371
+ - 'lib/fractor/queue_persister.rb'
372
+ - 'lib/fractor/result_cache.rb'
373
+
324
374
  # Offense count: 1
325
375
  # Configuration parameters: AllowedMethods.
326
376
  # AllowedMethods: respond_to_missing?
327
377
  Style/OptionalBooleanParameter:
328
378
  Exclude:
329
379
  - 'lib/fractor/workflow/job.rb'
380
+
381
+ # Offense count: 2
382
+ # This cop supports safe autocorrection (--autocorrect).
383
+ # Configuration parameters: EnforcedStyleForMultiline.
384
+ # SupportedStylesForMultiline: comma, consistent_comma, diff_comma, no_comma
385
+ Style/TrailingCommaInArguments:
386
+ Exclude:
387
+ - 'spec/examples/workflow/retry_workflow_spec.rb'
data/README.adoc CHANGED
@@ -82,6 +82,143 @@ puts "Results: #{supervisor.results.results.map(&:result)}"
82
82
  # => Results: [2, 4, 6]
83
83
  ----
84
84
 
85
+ === Timeout Configuration
86
+
87
+ Fractor supports flexible timeout configuration at three levels:
88
+
89
+ [cols="1,2,4"]
90
+ |===
91
+ |Level |Syntax |Description
92
+
93
+ |Global
94
+ |`Fractor.configure`
95
+ |Sets default timeout for all workers
96
+
97
+ |Worker
98
+ |`Worker.timeout` class method
99
+ |Sets timeout for all work processed by a worker
100
+
101
+ |Work item
102
+ |`Work.new(input, timeout: N)`
103
+ |Overrides worker timeout for specific work item
104
+ |===
105
+
106
+ [source,ruby]
107
+ ----
108
+ # 1. Global default (optional)
109
+ Fractor.configure do |config|
110
+ config.default_worker_timeout = 60 # 60 seconds
111
+ end
112
+
113
+ # 2. Worker-level timeout
114
+ class FastWorker < Fractor::Worker
115
+ timeout 10 # 10 second timeout for this worker
116
+
117
+ def process(work)
118
+ Fractor::WorkResult.new(result: work.input * 2, work: work)
119
+ end
120
+ end
121
+
122
+ # 3. Per-work-item timeout (overrides worker timeout)
123
+ class MyWork < Fractor::Work
124
+ def initialize(value, timeout: nil)
125
+ super({ value: value }, timeout: timeout)
126
+ end
127
+ end
128
+
129
+ supervisor = Fractor::Supervisor.new(
130
+ worker_pools: [{ worker_class: FastWorker }]
131
+ )
132
+
133
+ # Mix work items with different timeouts
134
+ fast_work = MyWork.new(1, timeout: 5) # 5 seconds
135
+ normal_work = MyWork.new(2) # uses worker's 10s timeout
136
+ slow_work = MyWork.new(3, timeout: 30) # 30 seconds
137
+
138
+ supervisor.add_work_items([fast_work, normal_work, slow_work])
139
+ supervisor.run
140
+ ----
141
+
142
+ When a timeout occurs, the work item is marked as failed with `error_category: :timeout` and can be retried if using the workflow system.
143
+
144
+ === Queue Persistence
145
+
146
+ For critical applications, work queues can be persisted to disk for crash recovery:
147
+
148
+ [source,ruby]
149
+ ----
150
+ # Create a persistent queue with automatic saving
151
+ queue = Fractor::PersistentWorkQueue.new("data/queue.json")
152
+
153
+ # Add work items - automatically saved
154
+ queue << MyWork.new(data1)
155
+ queue << MyWork.new(data2)
156
+
157
+ # Use with ContinuousServer for crash recovery
158
+ server = Fractor::ContinuousServer.new(
159
+ worker_pools: [{ worker_class: MyWorker }],
160
+ work_queue: queue
161
+ )
162
+
163
+ # Load any previous work items on startup
164
+ queue.load
165
+
166
+ server.run
167
+ ----
168
+
169
+ Supported persistence formats:
170
+ * **JSON** (default) - Human-readable, widely compatible
171
+ * **YAML** - More readable than JSON
172
+ * **Marshal** - Binary format, faster but Ruby-specific
173
+
174
+ === Result Caching
175
+
176
+ For expensive, deterministic operations, Fractor provides a result cache to avoid redundant processing of identical work items:
177
+
178
+ [source,ruby]
179
+ ----
180
+ # Create a cache with TTL (time-to-live)
181
+ cache = Fractor::ResultCache.new(ttl: 300) # 5 minutes
182
+
183
+ # Or with size limit
184
+ cache = Fractor::ResultCache.new(max_size: 1000)
185
+
186
+ # Or with memory limit
187
+ cache = Fractor::ResultCache.new(max_memory: 1024 * 1024) # 1MB
188
+
189
+ # Use the cache
190
+ result = cache.get(work) do
191
+ # This block only runs if work is not cached
192
+ expensive_operation(work)
193
+ end
194
+
195
+ # Check if work is cached
196
+ if cache.has?(work)
197
+ puts "Result is cached!"
198
+ end
199
+
200
+ # Manual cache operations
201
+ cache.set(work, result) # Store a result
202
+ cache.invalidate(work) # Remove a cached result
203
+ cache.clear # Remove all cached results
204
+
205
+ # Get cache statistics
206
+ stats = cache.stats
207
+ puts "Cache hit rate: #{stats[:hit_rate]}%"
208
+ ----
209
+
210
+ The cache generates consistent keys based on:
211
+ * Work class name
212
+ * Work input data
213
+ * Work timeout (if set)
214
+
215
+ This means identical work items with the same input and timeout will share cached results.
216
+
217
+ Cache eviction policies:
218
+ * **TTL** - Entries expire after a configured time
219
+ * **LRU** - Least-recently-used entries are evicted when max_size is reached
220
+ * **Memory-based** - Entries are evicted when max_memory is reached
221
+
85
222
  == Key features
86
223
 
87
224
  * *Function-driven*: Define processing logic by subclassing `Fractor::Worker`
@@ -0,0 +1,218 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "work_queue"
4
+ require_relative "queue_persister"
5
+ require "json"
6
+
7
+ module Fractor
8
+ # A work queue with persistence support.
9
+ # Automatically saves queue state to disk for crash recovery.
10
+ #
11
+ # @example Basic usage
12
+ # queue = PersistentWorkQueue.new("data/queue.json")
13
+ # queue << work_item # Automatically saved
14
+ #
15
+ # @example With custom persister
16
+ # persister = QueuePersister::YAMLPersister.new("data/queue.yml")
17
+ # queue = PersistentWorkQueue.new(persister: persister)
18
+ class PersistentWorkQueue < WorkQueue
19
+ attr_reader :persister
20
+
21
+ # Initialize a persistent work queue.
22
+ #
23
+ # @param path_or_persister [String, QueuePersister::Base] File path or persister instance
24
+ # @param auto_save [Boolean] Automatically save after each enqueue
25
+ # @param save_interval [Integer, nil] Seconds between auto-saves (nil = disabled)
26
+ def initialize(path_or_persister = nil, auto_save: true, save_interval: nil)
27
+ super()
28
+
29
+ @persister = case path_or_persister
30
+ when String
31
+ QueuePersister::JSONPersister.new(path_or_persister)
32
+ when QueuePersister::Base, nil
33
+ path_or_persister
34
+ else
35
+ raise ArgumentError,
36
+ "path_or_persister must be a String or QueuePersister::Base"
37
+ end
38
+
39
+ @auto_save = @persister ? auto_save : false
40
+ @save_interval = save_interval
41
+ @dirty = false
42
+ @save_thread = nil
43
+
44
+ # Start auto-save thread if interval is specified
45
+ start_save_thread if @save_interval
46
+ end
47
+
48
+ # Add a work item to the queue with automatic persistence.
49
+ #
50
+ # @param work_item [Fractor::Work] The work item to add
51
+ # @return [void]
52
+ def enqueue(work_item)
53
+ super
54
+ @dirty = true
55
+ save if @auto_save
56
+ end
57
+
58
+ # Alias for enqueue
59
+ #
60
+ # @param work_item [Fractor::Work] The work item to add
61
+ # @return [void]
62
+ def <<(work_item)
63
+ enqueue(work_item)
64
+ end
65
+
66
+ # Retrieve multiple work items from the queue.
67
+ # Marks queue as dirty for persistence.
68
+ #
69
+ # @param max_items [Integer] Maximum number of items to retrieve
70
+ # @return [Array<Fractor::Work>] Array of work items
71
+ def dequeue_batch(max_items = 10)
72
+ items = super
73
+ @dirty = true if items.any?
74
+ items
75
+ end
76
+
77
+ # Save the current queue state to disk.
78
+ #
79
+ # @return [Boolean] true if saved successfully
80
+ def save
81
+ return false unless @persister
82
+
83
+ # Get all items from the queue without removing them
84
+ items = peek_all
85
+ @persister.save(items)
86
+ @dirty = false
87
+ true
88
+ rescue StandardError => e
89
+ warn "Failed to save queue: #{e.message}"
90
+ false
91
+ end
92
+
93
+ # Load queue state from disk, restoring previous work items.
94
+ #
95
+ # @return [Integer] Number of items restored
96
+ def load
97
+ return 0 unless @persister
98
+
99
+ items = @persister.load
100
+ return 0 unless items
101
+
102
+ count = 0
103
+ items.each do |item|
104
+ # Convert hash back to Work object if needed
105
+ work = if item.is_a?(Hash)
106
+ deserialize_work(item)
107
+ else
108
+ item
109
+ end
110
+
111
+ @queue << work if work.is_a?(Fractor::Work)
112
+ count += 1
113
+ end
114
+
115
+ @dirty = false
116
+ count
117
+ rescue StandardError => e
118
+ warn "Failed to load queue: #{e.message}"
119
+ 0
120
+ end
121
+
122
+ # Clear the queue and remove persisted data.
123
+ #
124
+ # @return [Boolean] true if cleared successfully
125
+ def clear
126
+ @queue.clear
127
+ @persister&.clear
128
+ @dirty = false
129
+ true
130
+ rescue StandardError => e
131
+ warn "Failed to clear queue: #{e.message}"
132
+ false
133
+ end
134
+
135
+ # Check if the queue has unsaved changes.
136
+ #
137
+ # @return [Boolean] true if there are unsaved changes
138
+ def dirty?
139
+ @dirty
140
+ end
141
+
142
+ # Save and close the queue, cleaning up resources.
143
+ #
144
+ # @return [void]
145
+ def close
146
+ save if @dirty
147
+ stop_save_thread if @save_thread
148
+ end
149
+
150
+ private
151
+
152
+ # Peek at all items in the queue without removing them.
153
+ #
154
+ # @return [Array<Fractor::Work>] All work items in the queue
155
+ def peek_all
156
+ items = []
157
+ # Thread::Queue doesn't have a peek method, so we need to
158
+ # temporarily remove items, collect them, and put them back
159
+ temp = []
160
+ until @queue.empty?
161
+ item = @queue.pop(true)
162
+ temp << item
163
+ items << item
164
+ end
165
+ # Put items back
166
+ temp.each { |item| @queue << item }
167
+ items
168
+ rescue ThreadError
169
+ # Queue became empty during iteration
170
+ items
171
+ end
172
+
173
+ # Deserialize a work item from a hash.
174
+ #
175
+ # @param hash [Hash] The serialized work item
176
+ # @return [Fractor::Work, nil] The deserialized work item
177
+ def deserialize_work(hash)
178
+ return nil unless hash.is_a?(Hash)
179
+
180
+ # Extract class name and data (handle both string and symbol keys for JSON compatibility)
181
+ hash.delete(:_class) || hash.delete("_class")
182
+ input_data = hash.delete(:_input) || hash.delete("_input")
183
+ timeout = hash.delete(:_timeout) || hash.delete("_timeout")
184
+
185
+ # For simplicity, always use the base Work class with stored input
186
+ # This ensures correct deserialization without complex reflection
187
+ if timeout
188
+ Fractor::Work.new(input_data, timeout: timeout)
189
+ else
190
+ Fractor::Work.new(input_data)
191
+ end
192
+ rescue StandardError => e
193
+ warn "Failed to deserialize work item: #{e.message}"
194
+ nil
195
+ end
196
+
197
+ # Start the auto-save thread.
198
+ #
199
+ # @return [void]
200
+ def start_save_thread
201
+ @save_thread = Thread.new do
202
+ loop do
203
+ sleep @save_interval
204
+ save if @dirty
205
+ end
206
+ end
207
+ @save_thread.name = "PersistentWorkQueue-auto-save" if @save_thread.respond_to?(:name=)
208
+ end
209
+
210
+ # Stop the auto-save thread.
211
+ #
212
+ # @return [void]
213
+ def stop_save_thread
214
+ @save_thread&.kill
215
+ @save_thread = nil
216
+ end
217
+ end
218
+ end
@@ -0,0 +1,253 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require "yaml"
5
+
6
+ module Fractor
7
+ # Persistence strategies for work queues.
8
+ # Provides different backends for saving and loading queue state.
9
+ module QueuePersister
10
+ # Base class for queue persisters.
11
+ #
12
+ # @abstract Subclasses must implement {#save} and {#load}
13
+ class Base
14
+ # Save work items to persistent storage.
15
+ #
16
+ # @abstract
17
+ # @param items [Array<Fractor::Work>] Work items to save
18
+ # @return [Boolean] true if saved successfully
19
+ def save(_items)
20
+ raise NotImplementedError, "Subclasses must implement #save"
21
+ end
22
+
23
+ # Load work items from persistent storage.
24
+ #
25
+ # @abstract
26
+ # @return [Array<Hash>, nil] Serialized work items, or nil if no data
27
+ def load
28
+ raise NotImplementedError, "Subclasses must implement #load"
29
+ end
30
+
31
+ # Clear persisted data.
32
+ #
33
+ # @abstract
34
+ # @return [Boolean] true if cleared successfully
35
+ def clear
36
+ raise NotImplementedError, "Subclasses must implement #clear"
37
+ end
38
+
39
+ protected
40
+
41
+ # Serialize a work item to a hash.
42
+ #
43
+ # @param work [Fractor::Work] The work item to serialize
44
+ # @return [Hash] Serialized work item
45
+ def serialize_work(work)
46
+ hash = {
47
+ _class: work.class.name,
48
+ _input: work.input,
49
+ }
50
+ if work.respond_to?(:timeout) && !work.timeout.nil?
51
+ hash[:_timeout] =
52
+ work.timeout
53
+ end
54
+ hash
55
+ end
56
+ end
57
+
58
+ # JSON file persister.
59
+ # Stores queue state as a JSON array.
60
+ class JSONPersister < Base
61
+ # Initialize a JSON persister.
62
+ #
63
+ # @param path [String] Path to the JSON file
64
+ # @param pretty [Boolean] Format JSON with indentation
65
+ def initialize(path, pretty: true)
66
+ @path = path
67
+ @pretty = pretty
68
+ end
69
+
70
+ # Save work items to JSON file.
71
+ #
72
+ # @param items [Array<Fractor::Work>] Work items to save
73
+ # @return [Boolean] true if saved successfully
74
+ def save(items)
75
+ ensure_directory_exists
76
+
77
+ serialized = items.map { |work| serialize_work(work) }
78
+ json = @pretty ? JSON.pretty_generate(serialized) : JSON.generate(serialized)
79
+
80
+ File.write(@path, json)
81
+ true
82
+ rescue StandardError => e
83
+ warn "Failed to save to #{@path}: #{e.message}"
84
+ false
85
+ end
86
+
87
+ # Load work items from JSON file.
88
+ #
89
+ # @return [Array<Hash>, nil] Serialized work items, or nil if file doesn't exist
90
+ def load
91
+ return nil unless File.exist?(@path)
92
+
93
+ json = File.read(@path)
94
+ return nil if json.strip.empty?
95
+
96
+ JSON.parse(json)
97
+ rescue StandardError => e
98
+ warn "Failed to load from #{@path}: #{e.message}"
99
+ nil
100
+ end
101
+
102
+ # Clear the JSON file.
103
+ #
104
+ # @return [Boolean] true if cleared successfully
105
+ def clear
106
+ File.delete(@path) if File.exist?(@path)
107
+ true
108
+ rescue StandardError => e
109
+ warn "Failed to clear #{@path}: #{e.message}"
110
+ false
111
+ end
112
+
113
+ private
114
+
115
+ # Ensure the directory for the file exists.
116
+ #
117
+ # @return [void]
118
+ def ensure_directory_exists
119
+ dir = File.dirname(@path)
120
+ FileUtils.mkdir_p(dir) unless Dir.exist?(dir)
121
+ end
122
+ end
123
+
124
+ # YAML file persister.
125
+ # Stores queue state as a YAML document.
126
+ class YAMLPersister < Base
127
+ # Initialize a YAML persister.
128
+ #
129
+ # @param path [String] Path to the YAML file
130
+ def initialize(path)
131
+ @path = path
132
+ end
133
+
134
+ # Save work items to YAML file.
135
+ #
136
+ # @param items [Array<Fractor::Work>] Work items to save
137
+ # @return [Boolean] true if saved successfully
138
+ def save(items)
139
+ ensure_directory_exists
140
+
141
+ serialized = items.map { |work| serialize_work(work) }
142
+ yaml = YAML.dump(serialized)
143
+
144
+ File.write(@path, yaml)
145
+ true
146
+ rescue StandardError => e
147
+ warn "Failed to save to #{@path}: #{e.message}"
148
+ false
149
+ end
150
+
151
+ # Load work items from YAML file.
152
+ #
153
+ # @return [Array<Hash>, nil] Serialized work items, or nil if file doesn't exist
154
+ def load
155
+ return nil unless File.exist?(@path)
156
+
157
+ yaml = File.read(@path)
158
+ return nil if yaml.strip.empty?
159
+
160
+ YAML.safe_load(yaml,
161
+ permitted_classes: [Symbol, Hash, String, Integer, Float, TrueClass,
162
+ FalseClass, NilClass])
163
+ rescue StandardError => e
164
+ warn "Failed to load from #{@path}: #{e.message}"
165
+ nil
166
+ end
167
+
168
+ # Clear the YAML file.
169
+ #
170
+ # @return [Boolean] true if cleared successfully
171
+ def clear
172
+ File.delete(@path) if File.exist?(@path)
173
+ true
174
+ rescue StandardError => e
175
+ warn "Failed to clear #{@path}: #{e.message}"
176
+ false
177
+ end
178
+
179
+ private
180
+
181
+ # Ensure the directory for the file exists.
182
+ #
183
+ # @return [void]
184
+ def ensure_directory_exists
185
+ dir = File.dirname(@path)
186
+ FileUtils.mkdir_p(dir) unless Dir.exist?(dir)
187
+ end
188
+ end
189
+
190
+ # Marshal file persister.
191
+ # Uses Ruby's Marshal for binary serialization.
192
+ # Note: Marshal is not secure and should not be used with untrusted data.
193
+ class MarshalPersister < Base
194
+ # Initialize a Marshal persister.
195
+ #
196
+ # @param path [String] Path to the Marshal file
197
+ def initialize(path)
198
+ @path = path
199
+ end
200
+
201
+ # Save work items using Marshal.
202
+ #
203
+ # @param items [Array<Fractor::Work>] Work items to save
204
+ # @return [Boolean] true if saved successfully
205
+ def save(items)
206
+ ensure_directory_exists
207
+
208
+ serialized = items.map { |work| serialize_work(work) }
209
+ data = Marshal.dump(serialized)
210
+
211
+ File.binwrite(@path, data)
212
+ true
213
+ rescue StandardError => e
214
+ warn "Failed to save to #{@path}: #{e.message}"
215
+ false
216
+ end
217
+
218
+ # Load work items using Marshal.
219
+ #
220
+ # @return [Array<Hash>, nil] Serialized work items, or nil if file doesn't exist
221
+ def load
222
+ return nil unless File.exist?(@path)
223
+
224
+ data = File.binread(@path)
225
+ Marshal.load(data)
226
+ rescue StandardError => e
227
+ warn "Failed to load from #{@path}: #{e.message}"
228
+ nil
229
+ end
230
+
231
+ # Clear the Marshal file.
232
+ #
233
+ # @return [Boolean] true if cleared successfully
234
+ def clear
235
+ File.delete(@path) if File.exist?(@path)
236
+ true
237
+ rescue StandardError => e
238
+ warn "Failed to clear #{@path}: #{e.message}"
239
+ false
240
+ end
241
+
242
+ private
243
+
244
+ # Ensure the directory for the file exists.
245
+ #
246
+ # @return [void]
247
+ def ensure_directory_exists
248
+ dir = File.dirname(@path)
249
+ FileUtils.mkdir_p(dir) unless Dir.exist?(dir)
250
+ end
251
+ end
252
+ end
253
+ end
@@ -0,0 +1,274 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "digest"
4
+ require "json"
5
+
6
+ module Fractor
7
+ # Caches work results to avoid redundant processing of identical work items.
8
+ # Useful for expensive, deterministic operations.
9
+ #
10
+ # @example Basic usage
11
+ # cache = Fractor::ResultCache.new
12
+ # cached = cache.get(work) { work.process }
13
+ #
14
+ # @example With TTL
15
+ # cache = Fractor::ResultCache.new(ttl: 300) # 5 minutes
16
+ class ResultCache
17
+ attr_reader :hits, :misses
18
+
19
+ # Initialize a new result cache.
20
+ #
21
+ # @param ttl [Integer, nil] Time-to-live for cache entries in seconds (nil = no expiration)
22
+ # @param max_size [Integer, nil] Maximum number of entries (nil = unlimited)
23
+ # @param max_memory [Integer, nil] Maximum memory usage in bytes (nil = unlimited)
24
+ def initialize(ttl: nil, max_size: nil, max_memory: nil)
25
+ @cache = {}
26
+ @timestamps = {}
27
+ @access_times = {}
28
+ @ttl = ttl
29
+ @max_size = max_size
30
+ @max_memory = max_memory
31
+ @current_memory = 0
32
+ @mutex = Mutex.new
33
+ @hits = 0
34
+ @misses = 0
35
+ end
36
+
37
+ # Get the current cache size.
38
+ #
39
+ # @return [Integer] Number of entries in the cache
40
+ def size
41
+ @mutex.synchronize { @cache.size }
42
+ end
43
+
44
+ # Get a cached result or compute and cache it.
45
+ #
46
+ # @param work [Fractor::Work] The work item to process
47
+ # @yield Block to compute the result if not cached
48
+ # @return [WorkResult, Object] The cached or computed result
49
+ def get(work)
50
+ key = generate_key(work)
51
+
52
+ @mutex.synchronize do
53
+ # Check if we have a valid cached result
54
+ if @cache.key?(key) && !expired?(key)
55
+ @hits += 1
56
+ @access_times[key] = Time.now
57
+ return @cache[key]
58
+ end
59
+
60
+ @misses += 1
61
+
62
+ # Compute the result
63
+ result = yield
64
+
65
+ # Cache the result
66
+ cache_entry(key, result)
67
+
68
+ result
69
+ end
70
+ end
71
+
72
+ # Check if a work item has a cached result.
73
+ #
74
+ # @param work [Fractor::Work] The work item to check
75
+ # @return [Boolean] true if a valid cached result exists
76
+ def has?(work)
77
+ key = generate_key(work)
78
+
79
+ @mutex.synchronize do
80
+ @cache.key?(key) && !expired?(key)
81
+ end
82
+ end
83
+
84
+ # Store a result in the cache.
85
+ #
86
+ # @param work [Fractor::Work] The work item
87
+ # @param result [WorkResult, Object] The result to cache
88
+ # @return [void]
89
+ def set(work, result)
90
+ key = generate_key(work)
91
+
92
+ @mutex.synchronize do
93
+ cache_entry(key, result)
94
+ end
95
+ end
96
+
97
+ # Invalidate a cached result.
98
+ #
99
+ # @param work [Fractor::Work] The work item to invalidate
100
+ # @return [Boolean] true if a cached result was removed
101
+ def invalidate(work)
102
+ key = generate_key(work)
103
+
104
+ @mutex.synchronize do
105
+ if @cache.key?(key)
106
+ remove_entry(key)
107
+ true
108
+ else
109
+ false
110
+ end
111
+ end
112
+ end
113
+
114
+ # Clear all cached results.
115
+ #
116
+ # @return [void]
117
+ def clear
118
+ @mutex.synchronize do
119
+ @cache.clear
120
+ @timestamps.clear
121
+ @access_times.clear
122
+ @current_memory = 0
123
+ end
124
+ end
125
+
126
+ # Get cache statistics.
127
+ #
128
+ # @return [Hash] Cache statistics
129
+ def stats
130
+ @mutex.synchronize do
131
+ total = @hits + @misses
132
+ hit_rate = total.positive? ? (@hits.to_f / total * 100).round(2) : 0
133
+
134
+ {
135
+ size: @cache.size,
136
+ hits: @hits,
137
+ misses: @misses,
138
+ hit_rate: hit_rate,
139
+ current_memory: @current_memory,
140
+ }
141
+ end
142
+ end
143
+
144
+ # Remove expired entries from the cache.
145
+ #
146
+ # @return [Integer] Number of entries removed
147
+ def cleanup_expired
148
+ @mutex.synchronize do
149
+ expired_keys = @cache.keys.select { |key| expired?(key) }
150
+ expired_keys.each { |key| remove_entry(key) }
151
+ expired_keys.size
152
+ end
153
+ end
154
+
155
+ private
156
+
157
+ # Generate a cache key for a work item.
158
+ #
159
+ # @param work [Fractor::Work] The work item
160
+ # @return [String] The cache key
161
+ def generate_key(work)
162
+ # Create a deterministic key from the work item
163
+ data = {
164
+ class: work.class.name,
165
+ input: work.input,
166
+ }
167
+ if work.respond_to?(:timeout) && !work.timeout.nil?
168
+ data[:timeout] =
169
+ work.timeout
170
+ end
171
+
172
+ # Use SHA256 hash for consistent, collision-resistant keys
173
+ Digest::SHA256.hexdigest(JSON.dump(data))
174
+ end
175
+
176
+ # Check if a cache entry is expired.
177
+ #
178
+ # @param key [String] The cache key
179
+ # @return [Boolean] true if the entry is expired
180
+ def expired?(key)
181
+ return false unless @ttl
182
+
183
+ timestamp = @timestamps[key]
184
+ return true unless timestamp
185
+
186
+ Time.now - timestamp > @ttl
187
+ end
188
+
189
+ # Cache a result entry.
190
+ #
191
+ # @param key [String] The cache key
192
+ # @param result [Object] The result to cache
193
+ # @return [void]
194
+ def cache_entry(key, result)
195
+ # Evict oldest entry if max_size reached
196
+ evict_lru if @max_size && @cache.size >= @max_size
197
+
198
+ # Estimate memory usage
199
+ estimated_size = estimate_size(result)
200
+
201
+ # Evict entries if max_memory reached
202
+ evict_by_memory(estimated_size) if @max_memory
203
+
204
+ @cache[key] = result
205
+ @timestamps[key] = Time.now
206
+ @access_times[key] = Time.now
207
+ @current_memory += estimated_size
208
+ end
209
+
210
+ # Remove an entry from the cache.
211
+ #
212
+ # @param key [String] The cache key
213
+ # @return [void]
214
+ def remove_entry(key)
215
+ result = @cache.delete(key)
216
+ @timestamps.delete(key)
217
+ @access_times.delete(key)
218
+
219
+ if result
220
+ @current_memory -= estimate_size(result)
221
+ @current_memory = 0 if @current_memory.negative?
222
+ end
223
+ end
224
+
225
+ # Evict the least-recently-used entry.
226
+ #
227
+ # @return [void]
228
+ def evict_lru
229
+ return if @cache.empty?
230
+
231
+ # Find the entry with the oldest access time
232
+ lru_key = @access_times.min_by { |_, time| time }&.first
233
+ remove_entry(lru_key) if lru_key
234
+ end
235
+
236
+ # Evict entries to free memory.
237
+ #
238
+ # @param required_size [Integer] The size needed
239
+ # @return [void]
240
+ def evict_by_memory(required_size)
241
+ return unless @max_memory
242
+
243
+ while @current_memory + required_size > @max_memory && @cache.any?
244
+ evict_lru
245
+ end
246
+ end
247
+
248
+ # Estimate the memory size of an object.
249
+ #
250
+ # @param obj [Object] The object to measure
251
+ # @return [Integer] Estimated size in bytes
252
+ def estimate_size(obj)
253
+ # Rough estimation based on object inspection
254
+ case obj
255
+ when String
256
+ obj.bytesize
257
+ when Hash
258
+ obj.sum { |k, v| estimate_size(k.to_s) + estimate_size(v) }
259
+ when Array
260
+ obj.sum { |v| estimate_size(v) }
261
+ when Fractor::WorkResult
262
+ # Base size + result + error
263
+ size = 100 # Base object overhead
264
+ size += estimate_size(obj.result) if obj.result
265
+ size += estimate_size(obj.error) if obj.error
266
+ size
267
+ else
268
+ 100 # Default estimate for unknown objects
269
+ end
270
+ rescue StandardError
271
+ 100 # Fallback estimate
272
+ end
273
+ end
274
+ end
@@ -2,5 +2,5 @@
2
2
 
3
3
  module Fractor
4
4
  # Fractor version
5
- VERSION = "0.1.8"
5
+ VERSION = "0.1.9"
6
6
  end
data/lib/fractor/work.rb CHANGED
@@ -4,10 +4,16 @@ module Fractor
4
4
  # Base class for defining work items.
5
5
  # Contains the input data for a worker.
6
6
  class Work
7
- attr_reader :input
7
+ attr_reader :input, :timeout
8
8
 
9
- def initialize(input)
9
+ # Initializes a new work item.
10
+ #
11
+ # @param input [Object] The input data for the worker
12
+ # @param timeout [Numeric, nil] Optional per-work-item timeout in seconds.
13
+ # If nil, uses the worker's default timeout.
14
+ def initialize(input, timeout: nil)
10
15
  @input = input
16
+ @timeout = timeout
11
17
  end
12
18
 
13
19
  def to_s
@@ -23,6 +29,7 @@ module Fractor
23
29
  "@input=#{@input.inspect}",
24
30
  "@type=#{input.class.name}",
25
31
  ]
32
+ details << "@timeout=#{@timeout.inspect}" if @timeout
26
33
  details.join(" ")
27
34
  end
28
35
  end
@@ -63,21 +63,26 @@ module Fractor
63
63
  RactorLogger.debug("Received work #{work.inspect}", ractor_name: name)
64
64
 
65
65
  begin
66
- # Get the timeout for this worker (nil means no timeout)
67
- worker_timeout = worker.timeout
66
+ # Get the timeout for this specific work item
67
+ # Priority: work.timeout > worker.timeout (nil means no timeout)
68
+ work_timeout = if work.respond_to?(:timeout) && !work.timeout.nil?
69
+ work.timeout
70
+ else
71
+ worker.timeout
72
+ end
68
73
 
69
74
  # Process the work with timeout if configured
70
75
  # Note: Ruby's Timeout.timeout uses threads which don't work with Ractors.
71
76
  # We measure execution time and raise timeout error afterward if exceeded.
72
- result = if worker_timeout
77
+ result = if work_timeout
73
78
  start_time = Time.now
74
79
  process_result = worker.process(work)
75
80
  elapsed = Time.now - start_time
76
- if elapsed > worker_timeout
81
+ if elapsed > work_timeout
77
82
  # Raise a timeout error after the fact
78
83
  # Note: This is a post-facto timeout check - the work has already completed
79
84
  raise Timeout::Error,
80
- "execution timed out after #{elapsed}s (limit: #{worker_timeout}s)"
85
+ "execution timed out after #{elapsed}s (limit: #{work_timeout}s)"
81
86
  end
82
87
 
83
88
  process_result
@@ -99,7 +104,7 @@ module Fractor
99
104
  rescue Timeout::Error => e
100
105
  # Handle timeout errors as retriable errors
101
106
  RactorLogger.warn(
102
- "Timed out after #{worker.timeout}s processing work #{work.inspect}", ractor_name: name
107
+ "Timed out after #{work_timeout}s processing work #{work.inspect}", ractor_name: name
103
108
  )
104
109
  error_result = Fractor::WorkResult.new(
105
110
  error: "Worker timeout: #{e.message}",
@@ -89,21 +89,26 @@ module Fractor
89
89
  end
90
90
 
91
91
  begin
92
- # Get the timeout for this worker (nil means no timeout)
93
- worker_timeout = worker.timeout
92
+ # Get the timeout for this specific work item
93
+ # Priority: work.timeout > worker.timeout (nil means no timeout)
94
+ work_timeout = if work.respond_to?(:timeout) && !work.timeout.nil?
95
+ work.timeout
96
+ else
97
+ worker.timeout
98
+ end
94
99
 
95
100
  # Process the work with timeout if configured
96
101
  # Note: Ruby's Timeout.timeout uses threads which don't work with Ractors.
97
102
  # We measure execution time and raise timeout error afterward if exceeded.
98
- result = if worker_timeout
103
+ result = if work_timeout
99
104
  start_time = Time.now
100
105
  process_result = worker.process(work)
101
106
  elapsed = Time.now - start_time
102
- if elapsed > worker_timeout
107
+ if elapsed > work_timeout
103
108
  # Raise a timeout error after the fact
104
109
  # Note: This is a post-facto timeout check - the work has already completed
105
110
  raise Timeout::Error,
106
- "execution timed out after #{elapsed}s (limit: #{worker_timeout}s)"
111
+ "execution timed out after #{elapsed}s (limit: #{work_timeout}s)"
107
112
  end
108
113
 
109
114
  process_result
@@ -132,7 +137,7 @@ module Fractor
132
137
  rescue Timeout::Error => e
133
138
  # Handle timeout errors as retriable errors
134
139
  RactorLogger.warn(
135
- "Timed out after #{worker.timeout}s processing work #{work.inspect}", ractor_name: name
140
+ "Timed out after #{work_timeout}s processing work #{work.inspect}", ractor_name: name
136
141
  )
137
142
  error_result = Fractor::WorkResult.new(
138
143
  error: "Worker timeout: #{e.message}",
data/lib/fractor.rb CHANGED
@@ -7,6 +7,8 @@ require_relative "fractor/logger"
7
7
  require_relative "fractor/work"
8
8
  require_relative "fractor/work_result"
9
9
  require_relative "fractor/work_queue"
10
+ require_relative "fractor/queue_persister"
11
+ require_relative "fractor/persistent_work_queue"
10
12
  require_relative "fractor/priority_work"
11
13
  require_relative "fractor/priority_work_queue"
12
14
  require_relative "fractor/wrapped_ractor"
@@ -32,6 +34,7 @@ require_relative "fractor/error_statistics"
32
34
  require_relative "fractor/error_report_generator"
33
35
  require_relative "fractor/error_formatter"
34
36
  require_relative "fractor/execution_tracer"
37
+ require_relative "fractor/result_cache"
35
38
 
36
39
  # Optional: CLI (only load if Thor is available)
37
40
  begin
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: fractor
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.8
4
+ version: 0.1.9
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ronald Tse
@@ -194,9 +194,12 @@ files:
194
194
  - lib/fractor/performance_metrics_collector.rb
195
195
  - lib/fractor/performance_monitor.rb
196
196
  - lib/fractor/performance_report_generator.rb
197
+ - lib/fractor/persistent_work_queue.rb
197
198
  - lib/fractor/priority_work.rb
198
199
  - lib/fractor/priority_work_queue.rb
200
+ - lib/fractor/queue_persister.rb
199
201
  - lib/fractor/result_aggregator.rb
202
+ - lib/fractor/result_cache.rb
200
203
  - lib/fractor/shutdown_handler.rb
201
204
  - lib/fractor/signal_handler.rb
202
205
  - lib/fractor/supervisor.rb