ractor-pipeline 0.2.0
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 +7 -0
- data/LICENSE.txt +21 -0
- data/README.md +367 -0
- data/Rakefile +12 -0
- data/examples/demo.rb +138 -0
- data/examples/logstats.rb +114 -0
- data/examples/perf.rb +184 -0
- data/examples/readme_bench.rb +129 -0
- data/examples/vs_parallel.rb +116 -0
- data/lib/ractor/pipeline/version.rb +7 -0
- data/lib/ractor/pipeline.rb +462 -0
- data/sig/ractor/pipeline.rbs +6 -0
- metadata +56 -0
|
@@ -0,0 +1,462 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "pipeline/version"
|
|
4
|
+
|
|
5
|
+
# Ractor::Pipeline: a DSL to build stream processing pipelines with Ractors.
|
|
6
|
+
#
|
|
7
|
+
# include Ractor::Pipeline
|
|
8
|
+
#
|
|
9
|
+
# stream(File.foreach(name)).
|
|
10
|
+
# filter_pipe(lanes: 4){ it.include?("foo") }.
|
|
11
|
+
# reduce([0, 0, 0]) do |(lines, words, bytes), line|
|
|
12
|
+
# [lines + 1, words + line.scan(/\S+/).size, bytes + line.bytesize]
|
|
13
|
+
# end
|
|
14
|
+
#
|
|
15
|
+
# Execution model:
|
|
16
|
+
# * stream(source, batch: k) feeds the source into the pipeline from a
|
|
17
|
+
# Thread in the caller Ractor, k elements per message (default 1).
|
|
18
|
+
# Batching is transparent: stage blocks always see single elements.
|
|
19
|
+
# * pipe/filter_pipe/flat_pipe are persistent Ractor stages; each stage
|
|
20
|
+
# spawns `lanes:` (default 1) worker Ractors at terminal-operation
|
|
21
|
+
# time. Workers process many elements; no Ractor is created per
|
|
22
|
+
# element.
|
|
23
|
+
# * Boundaries into a multi-lane stage (lanes > 1) are demand-driven
|
|
24
|
+
# (pull): a lane worker sends a ready token upstream when it can take
|
|
25
|
+
# more work, and producers send a batch only to a worker they hold a
|
|
26
|
+
# token for (CREDIT tokens per producer/consumer pair). A busy worker
|
|
27
|
+
# stops sending tokens, so work never piles up behind it. Boundaries
|
|
28
|
+
# with a single consumer (lanes: 1 stages, and the terminal) are plain
|
|
29
|
+
# push.
|
|
30
|
+
# * When the head stage is multi-lane, the feeder reads the source only
|
|
31
|
+
# while it holds tokens, so a fast or infinite source cannot flood the
|
|
32
|
+
# pipeline.
|
|
33
|
+
# * tee broadcasts each element to every branch; branch outputs are
|
|
34
|
+
# merged (unordered) into one downstream stream.
|
|
35
|
+
# * reduce/each/to_a/count/first are terminal operations executed in the
|
|
36
|
+
# caller Ractor.
|
|
37
|
+
#
|
|
38
|
+
# Semantics notes:
|
|
39
|
+
# * Ordering: a chain of lanes: 1 stages preserves input order (also
|
|
40
|
+
# with batch:). Multi-lane stages are unordered.
|
|
41
|
+
# * End of stream is an in-band EOS message: each worker counts EOS from
|
|
42
|
+
# its upstream producers, drains its buffered output, and then
|
|
43
|
+
# broadcasts EOS downstream, so fan-in and fan-out shut down cleanly.
|
|
44
|
+
# * Cancellation (first, exceptions): the terminal closes its out_port
|
|
45
|
+
# and broadcasts a cancel message to every worker. Workers exit
|
|
46
|
+
# immediately, dropping their backlog; sends to closed ports raise
|
|
47
|
+
# Ractor::ClosedError and cascade the closure upstream (SIGPIPE-style).
|
|
48
|
+
# * Non-shareable objects are deep-copied at each Ractor boundary
|
|
49
|
+
# (Ractor::Port#send default). tee copies an element once per branch.
|
|
50
|
+
# * Stage blocks are isolated with Ractor.shareable_proc at DSL
|
|
51
|
+
# construction time: they may capture shareable outer values (they are
|
|
52
|
+
# snapshotted), and capturing a non-shareable value raises
|
|
53
|
+
# Ractor::IsolationError at pipe/filter_pipe/flat_pipe call time.
|
|
54
|
+
# * An exception raised in a stage block cancels the pipeline and is
|
|
55
|
+
# re-raised by the terminal operation in the caller Ractor.
|
|
56
|
+
# * Push links are unbounded (no backpressure); pull links are bounded
|
|
57
|
+
# by CREDIT batches per producer/consumer pair.
|
|
58
|
+
#
|
|
59
|
+
# Message protocol (one input Port per worker; messages are tagged):
|
|
60
|
+
# [:init, groups, ups, n_producers, batch_size]
|
|
61
|
+
# groups: [[ports, pull?], ...] one per fan-out destination
|
|
62
|
+
# [:data, batch, from] # from: replenish target on pull links, else nil
|
|
63
|
+
# [:ready, port] # demand token (pull links only)
|
|
64
|
+
# [:eos] # counted per upstream producer
|
|
65
|
+
# [:failure, exception] # forwarded to the terminal, which raises it
|
|
66
|
+
# [:cancel]
|
|
67
|
+
class Ractor
|
|
68
|
+
module Pipeline
|
|
69
|
+
class Error < StandardError; end
|
|
70
|
+
|
|
71
|
+
CREDIT = 2 # ready tokens per (producer, consumer) pair on pull links
|
|
72
|
+
|
|
73
|
+
STOP = Object.new # throw tag for early termination (caller Ractor only)
|
|
74
|
+
private_constant :STOP
|
|
75
|
+
|
|
76
|
+
Stage = Struct.new(:kind, :lanes, :job, :branches)
|
|
77
|
+
|
|
78
|
+
# Common stage-building vocabulary for Source and Branch.
|
|
79
|
+
module Stages
|
|
80
|
+
def stages = @stages ||= []
|
|
81
|
+
|
|
82
|
+
# Apply the block to each element and send the return value downstream.
|
|
83
|
+
def pipe(lanes: 1, &block)
|
|
84
|
+
add_stage(:pipe, lanes, block)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# Send the element itself downstream iff the block returns truthy.
|
|
88
|
+
def filter_pipe(lanes: 1, &block)
|
|
89
|
+
add_stage(:filter_pipe, lanes, block)
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# The block returns an each-able object; each of its elements is sent
|
|
93
|
+
# downstream (1 input -> N outputs).
|
|
94
|
+
def flat_pipe(lanes: 1, &block)
|
|
95
|
+
add_stage(:flat_pipe, lanes, block)
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# Broadcast each element to every branch. Branch outputs are merged
|
|
99
|
+
# (unordered) into one downstream stream.
|
|
100
|
+
#
|
|
101
|
+
# stream(src).tee(
|
|
102
|
+
# pipe{ A(it) },
|
|
103
|
+
# pipe{ B(it) },
|
|
104
|
+
# ).to_a
|
|
105
|
+
def tee(*branches)
|
|
106
|
+
branches.each do |br|
|
|
107
|
+
raise ArgumentError, "tee branch must be a stage list (use pipe{}/filter_pipe{})" unless Branch === br
|
|
108
|
+
raise ArgumentError, "tee branch must have at least one stage" if br.stages.empty?
|
|
109
|
+
end
|
|
110
|
+
stages << Stage.new(:tee, nil, nil, branches.map(&:stages))
|
|
111
|
+
self
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
private def add_stage(kind, lanes, block)
|
|
115
|
+
raise ArgumentError, "no block given" unless block
|
|
116
|
+
raise ArgumentError, "lanes: must be an Integer >= 1" unless Integer === lanes && lanes >= 1
|
|
117
|
+
|
|
118
|
+
# Isolate the block here so that capturing outer variables fails
|
|
119
|
+
# early, at DSL construction time.
|
|
120
|
+
job = Ractor.shareable_proc(&block)
|
|
121
|
+
stages << Stage.new(kind, lanes, job)
|
|
122
|
+
self
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
# A source-less pipeline fragment, used as a tee branch.
|
|
127
|
+
class Branch
|
|
128
|
+
include Stages
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
# A pipeline with a source. Terminal operations are defined here.
|
|
132
|
+
class Source
|
|
133
|
+
include Stages
|
|
134
|
+
|
|
135
|
+
def initialize(source, batch: 1)
|
|
136
|
+
raise ArgumentError, "source must respond to #each" unless source.respond_to?(:each)
|
|
137
|
+
raise ArgumentError, "batch: must be an Integer >= 1" unless Integer === batch && batch >= 1
|
|
138
|
+
@source = source
|
|
139
|
+
@batch = batch
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
# -- terminal operations (executed in the caller Ractor) -----------
|
|
143
|
+
|
|
144
|
+
def each(&block)
|
|
145
|
+
raise ArgumentError, "no block given" unless block
|
|
146
|
+
run { |msg| block.call(msg) }
|
|
147
|
+
self
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
def reduce(initial, &block)
|
|
151
|
+
raise ArgumentError, "no block given" unless block
|
|
152
|
+
acc = initial
|
|
153
|
+
run { |msg| acc = block.call(acc, msg) }
|
|
154
|
+
acc
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
def to_a
|
|
158
|
+
result = []
|
|
159
|
+
run { |msg| result << msg }
|
|
160
|
+
result
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
def count
|
|
164
|
+
n = 0
|
|
165
|
+
run { n += 1 }
|
|
166
|
+
n
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
# Terminates the pipeline as soon as enough elements are received.
|
|
170
|
+
def first(n = nil)
|
|
171
|
+
want = n || 1
|
|
172
|
+
result = []
|
|
173
|
+
if want > 0
|
|
174
|
+
run do |msg|
|
|
175
|
+
result << msg
|
|
176
|
+
throw STOP, true if result.size >= want
|
|
177
|
+
end
|
|
178
|
+
end
|
|
179
|
+
n ? result : result.first
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
private
|
|
183
|
+
|
|
184
|
+
# A concrete stage placement in the wiring graph. downs holds the
|
|
185
|
+
# fan-out destinations (Node or :out); producers the upstream Nodes
|
|
186
|
+
# (empty = fed by the source feeder).
|
|
187
|
+
Node = Struct.new(:kind, :lanes, :job, :downs, :producers, :ports)
|
|
188
|
+
|
|
189
|
+
# Builds Nodes for a stage list, from downstream to upstream.
|
|
190
|
+
# Returns the head destinations (what the previous producer feeds).
|
|
191
|
+
def plan(stage_list, downs, nodes)
|
|
192
|
+
stage_list.reverse_each.inject(downs) do |dwn, st|
|
|
193
|
+
if st.kind == :tee
|
|
194
|
+
st.branches.flat_map { |br_stages| plan(br_stages, dwn, nodes) }
|
|
195
|
+
else
|
|
196
|
+
node = Node.new(st.kind, st.lanes, st.job, dwn, [])
|
|
197
|
+
nodes << node
|
|
198
|
+
[node]
|
|
199
|
+
end
|
|
200
|
+
end
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
def run
|
|
204
|
+
nodes = []
|
|
205
|
+
heads = plan(stages, [:out], nodes)
|
|
206
|
+
nodes.each do |node|
|
|
207
|
+
node.downs.each { |dest| dest.producers << node if Node === dest }
|
|
208
|
+
end
|
|
209
|
+
n_out = nodes.sum { |node| node.downs.include?(:out) ? node.lanes : 0 }
|
|
210
|
+
n_out = 1 if stages.empty? # fed directly by the feeder
|
|
211
|
+
|
|
212
|
+
out_port = Ractor::Port.new
|
|
213
|
+
feed_port = Ractor::Port.new
|
|
214
|
+
ctrl = Ractor::Port.new
|
|
215
|
+
|
|
216
|
+
# spawn workers (they wait for [:init]) and collect their ports
|
|
217
|
+
nodes.each do |node|
|
|
218
|
+
node.lanes.times do |i|
|
|
219
|
+
Ractor.new(ctrl, node.job, node.kind, name: "pipeline/#{node.kind}[#{i}]") do |ctrl, job, kind|
|
|
220
|
+
Ractor::Pipeline.worker_loop(ctrl, job, kind)
|
|
221
|
+
end
|
|
222
|
+
end
|
|
223
|
+
node.ports = node.lanes.times.map { ctrl.receive }
|
|
224
|
+
end
|
|
225
|
+
all_ports = nodes.flat_map(&:ports)
|
|
226
|
+
|
|
227
|
+
# wire up
|
|
228
|
+
group_for = ->(dest) do
|
|
229
|
+
dest == :out ? [[out_port], false] : [dest.ports, dest.lanes > 1]
|
|
230
|
+
end
|
|
231
|
+
nodes.each do |node|
|
|
232
|
+
groups = node.downs.map(&group_for)
|
|
233
|
+
ups = if node.lanes > 1
|
|
234
|
+
node.producers.empty? ? [feed_port] : node.producers.flat_map(&:ports)
|
|
235
|
+
else
|
|
236
|
+
[]
|
|
237
|
+
end
|
|
238
|
+
npro = node.producers.empty? ? 1 : node.producers.sum(&:lanes)
|
|
239
|
+
node.ports.each { |port| port << [:init, groups, ups, npro, @batch] }
|
|
240
|
+
end
|
|
241
|
+
head_groups = heads.map(&group_for)
|
|
242
|
+
|
|
243
|
+
feeder = Thread.new { feed(feed_port, head_groups) }
|
|
244
|
+
|
|
245
|
+
eos = 0
|
|
246
|
+
early = catch(STOP) do
|
|
247
|
+
loop do
|
|
248
|
+
msg = out_port.receive
|
|
249
|
+
case msg[0]
|
|
250
|
+
when :data
|
|
251
|
+
msg[1].each { |obj| yield obj }
|
|
252
|
+
when :eos
|
|
253
|
+
eos += 1
|
|
254
|
+
break if eos == n_out
|
|
255
|
+
when :failure
|
|
256
|
+
shutdown(feeder, out_port, all_ports)
|
|
257
|
+
raise msg[1]
|
|
258
|
+
end
|
|
259
|
+
end
|
|
260
|
+
false
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
if early
|
|
264
|
+
shutdown(feeder, out_port, all_ports)
|
|
265
|
+
else
|
|
266
|
+
feeder.join
|
|
267
|
+
out_port.close
|
|
268
|
+
end
|
|
269
|
+
nil
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
# Feed the source, batching elements. Pull destinations receive a
|
|
273
|
+
# batch only when they granted a token, so the feeder never runs far
|
|
274
|
+
# ahead of a multi-lane head stage; push destinations are direct.
|
|
275
|
+
def feed(feed_port, head_groups)
|
|
276
|
+
tokens = Pipeline.token_pools(head_groups)
|
|
277
|
+
port_group = {}
|
|
278
|
+
head_groups.each_with_index do |(ports, pull), gi|
|
|
279
|
+
ports.each { |port| port_group[port] = gi } if pull
|
|
280
|
+
end
|
|
281
|
+
|
|
282
|
+
fill = lambda do |gi|
|
|
283
|
+
until (port = tokens[gi].take)
|
|
284
|
+
msg = feed_port.receive
|
|
285
|
+
case msg[0]
|
|
286
|
+
when :ready then tokens[port_group[msg[1]]].add(msg[1])
|
|
287
|
+
when :cancel then throw :cancelled
|
|
288
|
+
end
|
|
289
|
+
end
|
|
290
|
+
port
|
|
291
|
+
end
|
|
292
|
+
emit = lambda do |buf|
|
|
293
|
+
head_groups.each_with_index do |(ports, pull), gi|
|
|
294
|
+
if pull
|
|
295
|
+
fill.call(gi) << [:data, buf, feed_port]
|
|
296
|
+
else
|
|
297
|
+
ports.first << [:data, buf, nil]
|
|
298
|
+
end
|
|
299
|
+
end
|
|
300
|
+
end
|
|
301
|
+
|
|
302
|
+
catch(:cancelled) do
|
|
303
|
+
begin
|
|
304
|
+
buf = []
|
|
305
|
+
@source.each do |obj|
|
|
306
|
+
buf << obj
|
|
307
|
+
if buf.size >= @batch
|
|
308
|
+
emit.call(buf)
|
|
309
|
+
buf = []
|
|
310
|
+
end
|
|
311
|
+
end
|
|
312
|
+
emit.call(buf) unless buf.empty?
|
|
313
|
+
rescue Ractor::ClosedError
|
|
314
|
+
next # cancelled
|
|
315
|
+
rescue Exception => e
|
|
316
|
+
head_groups.first.first.first << [:failure, e] rescue nil
|
|
317
|
+
end
|
|
318
|
+
head_groups.each { |ports, _| ports.each { |port| port << [:eos] rescue nil } }
|
|
319
|
+
end
|
|
320
|
+
end
|
|
321
|
+
|
|
322
|
+
# Cancel: wake every worker (they may be idle or waiting for demand)
|
|
323
|
+
# and close out_port so in-flight sends fail and cascade upstream.
|
|
324
|
+
def shutdown(feeder, out_port, all_ports)
|
|
325
|
+
feeder.kill
|
|
326
|
+
feeder.join
|
|
327
|
+
all_ports.each { |port| port << [:cancel] rescue nil }
|
|
328
|
+
out_port.close
|
|
329
|
+
end
|
|
330
|
+
end
|
|
331
|
+
|
|
332
|
+
# Demand tokens of one pull group. Tokens are granted round-robin over
|
|
333
|
+
# the consumers (not in token arrival order): each consumer sends its
|
|
334
|
+
# CREDIT initial tokens in a burst, and handing them out in arrival
|
|
335
|
+
# order would cluster consecutive batches on the same worker.
|
|
336
|
+
class TokenPool
|
|
337
|
+
def initialize
|
|
338
|
+
@counts = Hash.new(0)
|
|
339
|
+
@order = []
|
|
340
|
+
end
|
|
341
|
+
|
|
342
|
+
def add(port)
|
|
343
|
+
@order << port unless @counts.key?(port)
|
|
344
|
+
@counts[port] += 1
|
|
345
|
+
end
|
|
346
|
+
|
|
347
|
+
def take
|
|
348
|
+
@order.size.times do
|
|
349
|
+
port = @order.shift
|
|
350
|
+
@order.push(port)
|
|
351
|
+
return port if @counts[port] > 0 && (@counts[port] -= 1 || true)
|
|
352
|
+
end
|
|
353
|
+
nil
|
|
354
|
+
end
|
|
355
|
+
end
|
|
356
|
+
|
|
357
|
+
class << self
|
|
358
|
+
def token_pools(groups)
|
|
359
|
+
groups.map { TokenPool.new }
|
|
360
|
+
end
|
|
361
|
+
|
|
362
|
+
# The main loop of a stage worker. Stateless between streams except
|
|
363
|
+
# for the demand bookkeeping of its (per-run) wiring.
|
|
364
|
+
def worker_loop(ctrl, job, kind)
|
|
365
|
+
in_port = Ractor::Port.new
|
|
366
|
+
ctrl << in_port
|
|
367
|
+
|
|
368
|
+
# wait for [:init]; queue anything that arrives earlier
|
|
369
|
+
early = []
|
|
370
|
+
msg = in_port.receive
|
|
371
|
+
until msg[0] == :init
|
|
372
|
+
early << msg
|
|
373
|
+
msg = in_port.receive
|
|
374
|
+
end
|
|
375
|
+
_, groups, ups, n_producers, batch_size = msg
|
|
376
|
+
|
|
377
|
+
tokens = Pipeline.token_pools(groups) # demand tokens, per pull group
|
|
378
|
+
pending = groups.map { [] } # batches waiting for a token
|
|
379
|
+
port_group = {}
|
|
380
|
+
groups.each_with_index do |(ports, pull), gi|
|
|
381
|
+
ports.each { |port| port_group[port] = gi } if pull
|
|
382
|
+
end
|
|
383
|
+
eos = 0
|
|
384
|
+
|
|
385
|
+
dispatch = lambda do |batch|
|
|
386
|
+
groups.each_with_index do |(ports, pull), gi|
|
|
387
|
+
if !pull
|
|
388
|
+
ports.first << [:data, batch, nil]
|
|
389
|
+
elsif (port = tokens[gi].take)
|
|
390
|
+
port << [:data, batch, in_port]
|
|
391
|
+
else
|
|
392
|
+
pending[gi] << batch
|
|
393
|
+
end
|
|
394
|
+
end
|
|
395
|
+
end
|
|
396
|
+
|
|
397
|
+
handle = lambda do |m|
|
|
398
|
+
case m[0]
|
|
399
|
+
when :data
|
|
400
|
+
begin
|
|
401
|
+
case kind
|
|
402
|
+
when :pipe
|
|
403
|
+
dispatch.call(m[1].map { |obj| job.call(obj) })
|
|
404
|
+
when :filter_pipe
|
|
405
|
+
out = m[1].select { |obj| job.call(obj) }
|
|
406
|
+
dispatch.call(out) unless out.empty?
|
|
407
|
+
when :flat_pipe
|
|
408
|
+
out = []
|
|
409
|
+
m[1].each { |obj| job.call(obj).each { |o| out << o } }
|
|
410
|
+
out.each_slice(batch_size) { |slice| dispatch.call(slice) }
|
|
411
|
+
end
|
|
412
|
+
rescue Ractor::ClosedError
|
|
413
|
+
raise
|
|
414
|
+
rescue Exception => e
|
|
415
|
+
groups.first.first.first << [:failure, e] rescue nil
|
|
416
|
+
end
|
|
417
|
+
(m[2] << [:ready, in_port] rescue nil) if m[2] # replenish credit
|
|
418
|
+
when :ready
|
|
419
|
+
gi = port_group[m[1]]
|
|
420
|
+
if (batch = pending[gi].shift)
|
|
421
|
+
m[1] << [:data, batch, in_port]
|
|
422
|
+
else
|
|
423
|
+
tokens[gi].add(m[1])
|
|
424
|
+
end
|
|
425
|
+
when :eos
|
|
426
|
+
eos += 1
|
|
427
|
+
when :failure
|
|
428
|
+
groups.first.first.first << m rescue nil # pass through
|
|
429
|
+
when :cancel
|
|
430
|
+
throw :cancelled
|
|
431
|
+
end
|
|
432
|
+
end
|
|
433
|
+
|
|
434
|
+
catch(:cancelled) do
|
|
435
|
+
ups.each { |up| CREDIT.times { up << [:ready, in_port] rescue nil } }
|
|
436
|
+
early.each { |m| handle.call(m) }
|
|
437
|
+
handle.call(in_port.receive) until eos == n_producers
|
|
438
|
+
# all producers finished: drain buffered batches, then EOS
|
|
439
|
+
handle.call(in_port.receive) until pending.all?(&:empty?)
|
|
440
|
+
groups.each { |ports, _| ports.each { |port| port << [:eos] rescue nil } }
|
|
441
|
+
end
|
|
442
|
+
rescue Ractor::ClosedError
|
|
443
|
+
# a consumer is gone: the stream was cancelled (SIGPIPE-style)
|
|
444
|
+
end
|
|
445
|
+
end
|
|
446
|
+
|
|
447
|
+
# -- DSL entry points ------------------------------------------------
|
|
448
|
+
|
|
449
|
+
def stream(source, batch: 1) = Source.new(source, batch:)
|
|
450
|
+
|
|
451
|
+
# A stream of exactly one element: stream1(obj) == stream([obj]).
|
|
452
|
+
# Use it to flow an object (even an each-able one) as a single element.
|
|
453
|
+
def stream1(obj) = Source.new([obj])
|
|
454
|
+
|
|
455
|
+
# Source-less fragments for tee branches.
|
|
456
|
+
def pipe(lanes: 1, &block) = Branch.new.pipe(lanes:, &block)
|
|
457
|
+
def filter_pipe(lanes: 1, &block) = Branch.new.filter_pipe(lanes:, &block)
|
|
458
|
+
def flat_pipe(lanes: 1, &block) = Branch.new.flat_pipe(lanes:, &block)
|
|
459
|
+
|
|
460
|
+
module_function :stream, :stream1, :pipe, :filter_pipe, :flat_pipe
|
|
461
|
+
end
|
|
462
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: ractor-pipeline
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.2.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Koichi Sasada
|
|
8
|
+
bindir: exe
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies: []
|
|
12
|
+
description: Ractor::Pipeline provides a shell-pipeline-like DSL (stream/pipe/filter_pipe/flat_pipe/tee/lanes)
|
|
13
|
+
where each stage is a persistent Ractor, making the execution topology visible in
|
|
14
|
+
the code. Multi-lane stages are fed by demand (pull) and sources can be batched
|
|
15
|
+
transparently.
|
|
16
|
+
email:
|
|
17
|
+
- ko1@atdot.net
|
|
18
|
+
executables: []
|
|
19
|
+
extensions: []
|
|
20
|
+
extra_rdoc_files: []
|
|
21
|
+
files:
|
|
22
|
+
- LICENSE.txt
|
|
23
|
+
- README.md
|
|
24
|
+
- Rakefile
|
|
25
|
+
- examples/demo.rb
|
|
26
|
+
- examples/logstats.rb
|
|
27
|
+
- examples/perf.rb
|
|
28
|
+
- examples/readme_bench.rb
|
|
29
|
+
- examples/vs_parallel.rb
|
|
30
|
+
- lib/ractor/pipeline.rb
|
|
31
|
+
- lib/ractor/pipeline/version.rb
|
|
32
|
+
- sig/ractor/pipeline.rbs
|
|
33
|
+
homepage: https://github.com/ko1/ractor-pipeline
|
|
34
|
+
licenses:
|
|
35
|
+
- MIT
|
|
36
|
+
metadata:
|
|
37
|
+
homepage_uri: https://github.com/ko1/ractor-pipeline
|
|
38
|
+
source_code_uri: https://github.com/ko1/ractor-pipeline
|
|
39
|
+
rdoc_options: []
|
|
40
|
+
require_paths:
|
|
41
|
+
- lib
|
|
42
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
43
|
+
requirements:
|
|
44
|
+
- - ">="
|
|
45
|
+
- !ruby/object:Gem::Version
|
|
46
|
+
version: '4.0'
|
|
47
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
48
|
+
requirements:
|
|
49
|
+
- - ">="
|
|
50
|
+
- !ruby/object:Gem::Version
|
|
51
|
+
version: '0'
|
|
52
|
+
requirements: []
|
|
53
|
+
rubygems_version: 4.0.6
|
|
54
|
+
specification_version: 4
|
|
55
|
+
summary: A DSL to build stream processing pipelines with Ractors.
|
|
56
|
+
test_files: []
|