orbitalqueue 0.0.2 → 0.0.3

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 (4) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +259 -2
  3. data/lib/orbitalqueue.rb +184 -12
  4. metadata +1 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d331ea29dd6bc80aa3c1127749f782845d1f23c2a9729f987af9128f1922bf21
4
- data.tar.gz: 5f7c04a0cef45399d4726900330ac96b1714e3275545a2d1e4e209fe61aec6c9
3
+ metadata.gz: 0ebd1ce38a6bd9bdc2acd6f36571966aeddddef703cedb97136d67b8cad64316
4
+ data.tar.gz: '061397dd5691647ddb49753f48d7f931fdc17840b4bda68857b3f2f1b1d88431'
5
5
  SHA512:
6
- metadata.gz: a7d5ead3e5494a3f38808e9b363b1ceb624168e294ee6a7165630092ec3ff3a58448234f434411f865539e89d8aa5f1cdf3100adde94ca6cc26066ec5c1d0795
7
- data.tar.gz: 7aab8b14af6f147b0b7276e37baee3db4324ca4052445c1fefcdd094f47e64e770f0a3357eec8a69d35ebbedd2263b7b3b5d208e457c73b5f6b3f34a5560ba39
6
+ metadata.gz: 37c7cd635345003b83446d3b82ce7036b91a0b3ba30913d41d62f7b06a0c618e7f6648784aab2c17661eb837b96330abedb06751a8db54f781587ae529daf2de
7
+ data.tar.gz: 3ee034e1f5d6ef47fb49ce744ea2355bf69ce54d8571c94436c1c092a04c448423e8ea2f01435eda36eecebf04e3a07980a45f66a94157895730d684e99faf71
data/README.md CHANGED
@@ -49,15 +49,272 @@ item.complete
49
49
 
50
50
  Calling `#pop` retrieves a single item from the queue in no particular order.
51
51
 
52
+ ```ruby
53
+ queue = OrbitalQueue.new("/path/to/queue")
54
+ item = queue.pop!
55
+
56
+ # Something, something...
57
+ ```
58
+
52
59
  The retrieved item enters a checkout state, and must be finalized by calling `#complete` once processing is finished.
53
60
 
54
61
  If guaranteed completion is not required and the item should be removed immediately upon retrieval, use `#pop!` instead.
55
62
 
56
- Both `#pop` and `#pop!` return a queue item as a `Hash`. If the original object was not a `Hash`, it will be stored under the `:data` key. Regardless of the original type, the queue ID is always stored under the `:queue_id` key.
63
+ Both `#pop` and `#pop!` methods returns `OrbitalQueue::QueueObject` object.
64
+ You can access queue data via `OrbitalQueue::QueueObject#data`.
65
+
66
+ When calling `#complete`, `#destruct`, or `#defer` directly on `OrbitalQueue`, `queue_id` must be given.
67
+ If you use these methods via `OrbitalQueue::QueueObject`, the `queue_id` is internally handled and can be omitted.
68
+
69
+ ### Dequeue with block
70
+
71
+ You can call `#pop` with a block.
72
+
73
+ When call with block, block is called with queue data as an argument.
74
+
75
+ queue item automatically complete when blocke ends without error.
76
+
77
+ The `#pop` method can be called with an optional block.
78
+ When used this way, the block is invoked with the item's data.
79
+ After successful execution (without exceptions), the item is considered complete and removed from the queue.
80
+
81
+ ```ruby
82
+ queue = OrbitalQueue.new("/path/to/queue")
83
+ queue.pop do |data|
84
+ #...
85
+ end
86
+ ```
87
+
88
+ ### Dequeue loop
89
+
90
+ While technically possible to loop over `#pop`, it doesn't support block-based iteration.
91
+
92
+ Use `#each` when you want to process items with a block in a loop.
93
+ It provides a clean and idiomatic Ruby style for sequential processing.
94
+
95
+ For full control over each item, use `#each_item`, which yields a `QueueObject` instead of just the data.
96
+
97
+ `#each_item` iterates over queue items as `QueueObject` instances, not raw data.
98
+ This allows direct control over each job—for example, deferring its execution using `#defer`.
99
+
100
+ Unlike `#each`, which automatically marks items as complete after the block runs,
101
+ `#each_item` exposes queue control for cases where completion isn't guaranteed or deferred handling is needed.
102
+
103
+ ### Job deferral
104
+
105
+ `OrbitalQueue` supports job deferral, enabling queue items to be scheduled for retry or postponed execution with precise control.
106
+
107
+ Calling `#defer` transitions a queue item into the deferred state.
108
+ This moves the item's file into the `.defer` directory and creates a retry metadata file in `.retry`.
109
+
110
+ Once an item has been deferred, it is associated with retry information, referred to as `retry_data`.
111
+ This is a `Hash` object that tracks rescheduling behavior.
112
+
113
+ `#defer` uses `retry_data` to determine whether the item can be retried, including retry count limits.
114
+ When called with a block, `#defer` yields `retry_data` as an argument, allowing custom modifications.
115
+
116
+ Regardless of how it's called, `retry_data` is persisted after the method returns.
117
+ To control deferral behavior, modify values inside `retry_data`—typically by changing the `:until` field (Unix timestamp).
118
+ This field specifies when the item should become eligible for re-queueing, making it ideal for implementing backoff strategies.
119
+
120
+ ```ruby
121
+ queue.each_item do |item|
122
+ begin
123
+ # Something...
124
+ rescue
125
+ item.defer(Time.now + 300) # Retry after 5 minutes.
126
+ end
127
+ end
128
+ ```
129
+
130
+ with block:
131
+
132
+ ```ruby
133
+ queue.each_item do |item|
134
+ begin
135
+ # Something...
136
+ rescue
137
+ item.defer do |retry_item|
138
+ if retry_item[:count] > 5
139
+ item.destruct
140
+ else
141
+ retry_item[:until] = Time.now + 300 * retry_item[:count]
142
+ end
143
+ end
144
+ end
145
+ end
146
+ ```
147
+
148
+ The `#defer` method's main role is to move the queue item file into the `.defer` directory.
149
+ Since `OrbitalQueue` operates without a server, it cannot scan `.defer` efficiently or restore deferred items automatically.
150
+
151
+ Aside from the internal keys `:count` and `:until`, all other values in `retry_data` are preserved as-is.
152
+ You can freely store custom metadata inside it—such as failure reasons or backoff parameters.
153
+
154
+ The `#destruct` method removes all files associated with a queue item and raises `OrbitalQueue::ItemDestructed`.
155
+ This exception is caught inside `#defer`, allowing `#destruct` to abort the entire deferral process.
156
+
157
+ ⚠️ Do not rescue `OrbitalQueue::ItemDestructed` within a `#defer` block.
158
+ If the block completes normally after destruction, queue integrity may be violated.
159
+
160
+ The `#archive` method creates a Marshal-serialized file under `.archive`, containing the original data, its `retry_data`, and an `archiveinfo` hash.
161
+ After archiving, it calls `#destruct` to remove the live queue item.
162
+
163
+ Archived files are never accessed by OrbitalQueue itself.
164
+
165
+ Note: Because `archive` discards in-memory `retry_data`, you cannot modify it before archiving.
166
+ Instead, extra metadata should be passed as arguments to `archive` and will be merged into `archiveinfo`.
167
+
168
+ ```ruby
169
+ queue.each_item do |item|
170
+ begin
171
+ #...
172
+ rescue
173
+ item.defer do |retry_data|
174
+ if retry_data[:count] > 5
175
+ item.archive({reason: "Host timeout"})
176
+ end
177
+ end
178
+ end
179
+ end
180
+ ```
181
+
182
+ ### Resume deferred job
183
+
184
+ Deferred queue items must be manually restored using the `resume` method.
185
+ This method is typically executed by a separate worker from the one handling regular queue operations.
186
+
187
+ It is defined as an instance method:
188
+
189
+ ```ruby
190
+ queue = OrbitalQueue.new("/path/to/queue")
191
+ queue.resume
192
+ ```
193
+
194
+ For convenience, resume can also be called as a class method:
195
+
196
+ ```ruby
197
+ OrbitalQueue.resume("/path/to/queue")
198
+ ```
57
199
 
58
200
  # About Orbital Design
59
201
 
60
202
  ## Description
61
203
 
62
- ## Design pattern rules
204
+ Orbital Design is a programming pattern optimized for distributed systems.
205
+ It is especially well-suited to environments where:
206
+
207
+ * New data constantly arrives without pause
208
+ * Processing workloads vary in complexity and demand asymmetric distribution
209
+ * Systems start small but must scale seamlessly to clustered deployments
210
+
211
+ ## Philosophy
212
+
213
+ Orbital Design distinguishes between "agents" and "workers".
214
+ In most cases, an agent refers to a program, while a worker is a process.
215
+
216
+ The core principle is that **workers only need to care about what they do**.
217
+ Upon starting, a worker picks a single available job prepared for it and executes it—no coordination or negotiation required.
218
+
219
+ This behavior mirrors that of individuals in a larger society, or cells within a living organism.
220
+ Each unit performs its specific role independently.
221
+
222
+ This philosophy is deeply aligned with the Unix principle:
223
+ _"Do one thing, and do it well."_
224
+
225
+ ## Core Rules of Orbital Design
226
+
227
+ Orbital Design defines a set of principles to preserve decoupling, clarity, and safety in distributed systems:
228
+
229
+ - *Agents must remain small and focused*.
230
+ Each agent is responsible for doing one thing, and doing it well.
231
+
232
+ - *Workers must not access data unrelated to their task*, nor inspect other workers' state or progress.
233
+
234
+ - *Write access to a database or dataset must be held by exactly one agent*.
235
+ This prevents conflicting updates and maintains integrity.
236
+
237
+ - *Deletion from a database may only be performed by:*
238
+ - A worker with exclusive read access to the data, or
239
+ - A sweeper worker that receives notifications from all readers
240
+
241
+ - *Agents must not block on I/O*.
242
+ Blocking input/output disrupts concurrency and undermines distributed fairness.
243
+
244
+ ## Benefits of Orbital Design
245
+
246
+ ### Ease of Implementation
247
+
248
+ Each program is small and focused, with clearly defined responsibilities.
249
+ Because agents cannot access global state and avoid blocking operations, race conditions are structurally prevented.
250
+
251
+ This allows each unit to concentrate solely on its task—no need to worry about concurrency or system state.
252
+
253
+ ## Simplicity
254
+
255
+ Orbital Design requires minimal complexity.
256
+ It does not depend on heavy frameworks or advanced techniques.
257
+
258
+ It can be fully implemented using standard OS features such as file systems, processes, and signals.
259
+ No special measures are needed to achieve scalability.
260
+
261
+ ## Language Agnosticism
262
+
263
+ Programs are isolated and do not interfere with one another.
264
+ This allows you to implement each agent in any language that suits the task.
265
+
266
+ You can choose a language based on convenience, libraries, or performance.
267
+ Critical paths can be written in C, C++, Rust, or Nim as needed, while simpler agents may use scripting languages.
268
+
269
+ Even agents with similar functionality can be written in different languages depending on input format or operational context.
270
+
271
+ ## Parallelism and Decomposition
272
+
273
+ Restricted I/O paths eliminate contention during parallel execution.
274
+ By following the design pattern, concurrency becomes straightforward.
275
+
276
+ No locking or synchronization is required—so parallel processing not only becomes easier to write, but also more performance-effective.
277
+
278
+ Additionally, replacing I/O layers with network interfaces naturally extends the system into distributed computing.
279
+
280
+ ## Signal Friendliness
281
+
282
+ Although OS-level signals are simple and often underutilized for concurrency,
283
+ Orbital Design enables practical use of signals for multi-worker environments.
284
+
285
+ This can provide a minor advantage when building cooperative worker pools.
286
+
287
+ ## Compatibility with Systemd
288
+
289
+ Systemd's `@.service` unit files support multi-instance execution.
290
+
291
+ Agents designed with Orbital Design require no more than a worker name as an argument, making them trivially scalable to multiple instances.
292
+ This provides a low-effort pathway to multi-worker deployments, with restarts handled by Systemd itself.
293
+
294
+ ## Compatibility with Job Schedulers
295
+
296
+ Orbital Design is not limited to multi-worker or multi-instance models.
297
+ It is especially well-suited to systems that rely on periodic execution by job schedulers.
298
+
299
+ While worker-driven systems react to runtime state, job schedulers operate on time-based triggers.
300
+ Thanks to its stateless model, Orbital Design allows agents to run regardless of timing or system condition.
301
+
302
+ ## Shell Script Friendly
303
+
304
+ Each agent has a clear and narrow scope, with no need for shared database schemas.
305
+ This makes it easy to write parts of the system in shell scripts where appropriate.
306
+
307
+ In practice, this results in simpler and more maintainable solutions in more cases than expected.
308
+
309
+ ## Replaceable Components
310
+
311
+ Programs in Orbital Design are small and well-scoped.
312
+ When a language, library, or performance characteristic becomes a limitation, swapping out components comes at low cost.
313
+
314
+ This helps maintain long-term system health and avoids software decay.
315
+
316
+ # Finally
317
+
318
+ “The library is minimal. The idea is not.”
63
319
 
320
+ Orbital Design is not a framework. It's a way of thinking. It thrives where ideas are shared freely.
data/lib/orbitalqueue.rb CHANGED
@@ -14,19 +14,29 @@ class OrbitalQueue
14
14
  class QueueUnexisting < QueueError
15
15
  end
16
16
 
17
+ class ItemDestructed < QueueError
18
+ end
19
+
20
+ # Return deferred item to queue
21
+ def self.resume dir
22
+ self.new(dir).resume
23
+ end
24
+
17
25
  # Create queue master in presented dir.
18
26
  #
19
- # dir: Queue directory
20
- # create: If true is given, creates the queue directory when it is missing
27
+ # dir:: Queue directory
28
+ # create:: If true is given, creates the queue directory when it is missing
21
29
  def initialize dir, create=false
22
30
  @queue_dir = dir
23
31
 
24
- unless File.exist?(File.join(dir, ".checkout"))
25
- if create
26
- require 'fileutils'
27
- FileUtils.mkdir_p(File.join(dir, ".checkout"))
28
- else
29
- raise QueueUnexisting.new("Queue directory #{dir} does not exist.")
32
+ %w:.checkout .defer .retry .archive:.each do |subdir|
33
+ unless File.exist?(File.join(dir, subdir))
34
+ if create
35
+ require 'fileutils'
36
+ FileUtils.mkdir_p(File.join(dir, subdir))
37
+ else
38
+ raise QueueUnexisting.new("Queue directory #{dir} does not exist.")
39
+ end
30
40
  end
31
41
  end
32
42
  end
@@ -47,6 +57,10 @@ class OrbitalQueue
47
57
  # Popped queue items are placed in the checkout directory. After processing is complete, +#complete+ must be called to remove the item from the queue.
48
58
  #
49
59
  # If block is given, complete automatically after yield.
60
+ #
61
+ # :call-seq:
62
+ # pop() -> queue_object
63
+ # pop() {|data| ... } -> queue_id
50
64
  def pop
51
65
  queue_data = nil
52
66
  queue_id = nil
@@ -66,7 +80,7 @@ class OrbitalQueue
66
80
  break
67
81
  end
68
82
 
69
- if block_given?
83
+ if queue_data && block_given?
70
84
  yield queue_data.data
71
85
  complete queue_id
72
86
  else
@@ -74,7 +88,10 @@ class OrbitalQueue
74
88
  end
75
89
  end
76
90
 
77
- # Pop data from queue and remove it from queue.
91
+ # Pop data and remove it from queue.
92
+ #
93
+ # :call-seq:
94
+ # pop!() -> queue_object
78
95
  def pop!
79
96
  queue_item = pop
80
97
  if queue_item
@@ -84,16 +101,137 @@ class OrbitalQueue
84
101
  queue_item
85
102
  end
86
103
 
104
+ # Iterate each queue item data.
105
+ def each
106
+ while item = pop
107
+ yield item.data
108
+ item.complete
109
+ end
110
+ end
111
+
112
+ # Iterate each queue item.
113
+ def each_item
114
+ while item = pop
115
+ yield item
116
+ item.complete unless item.deferred?
117
+ end
118
+ end
119
+
87
120
  # Remove checked out queue item.
88
121
  def complete queue_id
89
122
  begin
90
- File.delete(File.join(@queue_dir, ".checkout", (queue_id + ".marshal")))
123
+ checkout_file = File.join(@queue_dir, ".checkout", (queue_id + ".marshal"))
124
+ retry_file = File.join(@queue_dir, ".retry", (queue_id + ".marshal"))
125
+ File.delete(checkout_file)
126
+ File.delete(retry_file) if File.exist?(retry_file)
91
127
  rescue SystemCallError => e
92
128
  raise QueueRemoveError, "Failed to complete queue #{queue_id}: #{e.class}"
93
129
  end
94
130
 
95
131
  queue_id
96
132
  end
133
+
134
+ # Delete all related files with queue_id, and raise ItemDectructed exception.
135
+ def destruct queue_id
136
+ queue_files = Dir.glob([@queue_dir, "**", (queue_id + ".marshal")].join("/"), File::FNM_DOTMATCH)
137
+ File.delete(*queue_files) unless queue_files.empty?
138
+ raise ItemDestructed, "#{queue_id} is destructed."
139
+ end
140
+
141
+ # Archive current queue relative data and call +destruct+.
142
+ # This method should be called from QueueObject.
143
+ def archive queue_id, data, archiveinfo_additional={} # :nodoc:
144
+ archiveinfo = archiveinfo_additional.merge({
145
+ archived_at: Time.now.to_i
146
+ })
147
+
148
+ retry_data = load_retryobj queue_id
149
+
150
+ archive_data = {
151
+ archiveinfo: archiveinfo,
152
+ retry_data: retry_data,
153
+ data: data
154
+ }
155
+
156
+ File.open(File.join(@queue_dir, ".archive", (["archive", archiveinfo[:archived_at], queue_id].join("-") + ".marshal")), "w") {|f| Marshal.dump archive_data, f}
157
+
158
+ destruct queue_id
159
+ end
160
+
161
+ # Mark queue item as deferred.
162
+ #
163
+ # :call-seq:
164
+ # defer(queue_id, time_at, max_count=nil) -> retry_data | nil
165
+ # defer() {|retry_data| ... } -> retry_data | nil
166
+ def defer queue_id, time_at=nil, max_count=nil
167
+ retry_data = load_retryobj queue_id
168
+ retry_data[:count] += 1
169
+ if block_given?
170
+ yield retry_data
171
+ else
172
+ unless time_at
173
+ raise ArgumentError, "time_at is required when no block is given."
174
+ end
175
+
176
+ if max_count && retry_data[:count] > max_count
177
+ destruct queue_id
178
+ end
179
+ retry_data[:until] = time_at.to_i
180
+ end
181
+
182
+ dump_retryobj queue_id, retry_data
183
+
184
+ checkout_path = File.join(@queue_dir, ".checkout", (queue_id) + ".marshal")
185
+ defer_path = File.join(@queue_dir, ".defer", (queue_id) + ".marshal")
186
+ File.rename checkout_path, defer_path
187
+
188
+ retry_data
189
+ rescue ItemDestructed
190
+ nil
191
+ end
192
+
193
+ # Return deferred item to queue.
194
+ def resume
195
+ now = Time.now.to_i
196
+ deferred_files = Dir.children(File.join(@queue_dir, ".retry"))
197
+ deferred_files.each do |fn|
198
+ retry_path = File.join(@queue_dir, ".retry", fn)
199
+ retry_data = Marshal.load File.read retry_path
200
+
201
+ if retry_data[:until] < now
202
+ queue_path = File.join(@queue_dir, fn)
203
+ defer_path = File.join(@queue_dir, ".defer", fn)
204
+ File.rename(defer_path, queue_path)
205
+ end
206
+ end
207
+
208
+ nil
209
+ end
210
+
211
+ private
212
+
213
+ # Save to .retry
214
+ def dump_retryobj queue_id, data
215
+ retry_path = File.join(@queue_dir, ".retry", (queue_id) + ".marshal")
216
+ File.open(retry_path, "w") {|f| Marshal.dump data, f }
217
+ nil
218
+ end
219
+
220
+ # Load from .retry
221
+ def load_retryobj queue_id
222
+ retry_path = File.join(@queue_dir, ".retry", (queue_id) + ".marshal")
223
+ retry_data = nil
224
+ if File.exist? retry_path
225
+ retry_data = Marshal.load File.read retry_path
226
+ else
227
+ retry_data = {
228
+ count: 0,
229
+ until: nil
230
+ }
231
+ end
232
+
233
+ retry_data
234
+ end
97
235
  end
98
236
 
99
237
  # Queue item capsule.
@@ -103,6 +241,7 @@ class OrbitalQueue::QueueObject
103
241
  @data = data
104
242
  @queue_id = queue_id
105
243
  @completed = false
244
+ @deferred = false
106
245
  end
107
246
 
108
247
  attr_reader :data
@@ -118,8 +257,41 @@ class OrbitalQueue::QueueObject
118
257
  end
119
258
  end
120
259
 
260
+ # Wrap for the end of queue item.
261
+ def destruct
262
+ @completed = true
263
+ @queue.destruct(@queue_id)
264
+ end
265
+
266
+ # Archive current queue relative data and call +destruct+.
267
+ def archive archiveinfo_additional={}
268
+ @completed = true
269
+ @queue.archive @queue_id, @data, archiveinfo_additional
270
+ end
271
+
121
272
  # Terrible redundunt method.
122
- def complete?
273
+ def complete? # :nodoc:
123
274
  @completed
124
275
  end
276
+
277
+ # Retry later.
278
+ #
279
+ # time_at:: Deferring retry until this time
280
+ # max_count:: Retry count limit
281
+ #
282
+ # :call-seq:
283
+ # defer(time_at, max_count=nil) -> retry_data
284
+ # defer() {|retry_data| ... } -> retry_data
285
+ def defer time_at=nil, max_count=nil, &block
286
+ if block
287
+ @queue.defer(@queue_id, &block)
288
+ else
289
+ @queue.defer(@queue_id, time_at, max_count)
290
+ end
291
+ @deferred = true
292
+ end
293
+
294
+ def deferred?
295
+ @deferred
296
+ end
125
297
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: orbitalqueue
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.2
4
+ version: 0.0.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Masaki Haruka