omq 0.5.1 → 0.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +184 -0
  3. data/README.md +21 -19
  4. data/exe/omq +6 -0
  5. data/lib/omq/cli/base_runner.rb +423 -0
  6. data/lib/omq/cli/channel.rb +8 -0
  7. data/lib/omq/cli/client_server.rb +106 -0
  8. data/lib/omq/cli/config.rb +51 -0
  9. data/lib/omq/cli/formatter.rb +75 -0
  10. data/lib/omq/cli/pair.rb +31 -0
  11. data/lib/omq/cli/peer.rb +8 -0
  12. data/lib/omq/cli/pipe.rb +249 -0
  13. data/lib/omq/cli/pub_sub.rb +14 -0
  14. data/lib/omq/cli/push_pull.rb +14 -0
  15. data/lib/omq/cli/radio_dish.rb +27 -0
  16. data/lib/omq/cli/req_rep.rb +77 -0
  17. data/lib/omq/cli/router_dealer.rb +70 -0
  18. data/lib/omq/cli/scatter_gather.rb +14 -0
  19. data/lib/omq/cli.rb +468 -0
  20. data/lib/omq/pub_sub.rb +2 -2
  21. data/lib/omq/radio_dish.rb +2 -2
  22. data/lib/omq/socket.rb +74 -27
  23. data/lib/omq/version.rb +1 -1
  24. data/lib/omq/zmtp/connection.rb +24 -3
  25. data/lib/omq/zmtp/engine.rb +179 -17
  26. data/lib/omq/zmtp/options.rb +4 -3
  27. data/lib/omq/zmtp/reactor.rb +10 -5
  28. data/lib/omq/zmtp/routing/channel.rb +8 -2
  29. data/lib/omq/zmtp/routing/fan_out.rb +38 -8
  30. data/lib/omq/zmtp/routing/pair.rb +8 -2
  31. data/lib/omq/zmtp/routing/peer.rb +7 -1
  32. data/lib/omq/zmtp/routing/push.rb +14 -7
  33. data/lib/omq/zmtp/routing/radio.rb +32 -11
  34. data/lib/omq/zmtp/routing/rep.rb +11 -7
  35. data/lib/omq/zmtp/routing/req.rb +1 -2
  36. data/lib/omq/zmtp/routing/round_robin.rb +35 -1
  37. data/lib/omq/zmtp/routing/router.rb +7 -1
  38. data/lib/omq/zmtp/routing/scatter.rb +16 -3
  39. data/lib/omq/zmtp/routing/server.rb +7 -1
  40. data/lib/omq/zmtp/routing/xsub.rb +7 -1
  41. data/lib/omq/zmtp/transport/inproc.rb +40 -5
  42. data/lib/omq/zmtp/transport/ipc.rb +9 -7
  43. data/lib/omq/zmtp/transport/tcp.rb +14 -7
  44. data/lib/omq/zmtp/writable.rb +21 -4
  45. data/lib/omq.rb +7 -0
  46. metadata +18 -3
  47. data/exe/omqcat +0 -532
@@ -0,0 +1,249 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OMQ
4
+ module CLI
5
+ class PipeRunner
6
+ attr_reader :config
7
+
8
+
9
+ def initialize(config)
10
+ @config = config
11
+ @fmt = Formatter.new(config.format, compress: config.compress)
12
+ end
13
+
14
+
15
+ def call(task)
16
+ if config.parallel
17
+ run_parallel
18
+ else
19
+ run_sequential(task)
20
+ end
21
+ end
22
+
23
+
24
+ private
25
+
26
+
27
+ def run_sequential(task)
28
+ pull_ep = config.endpoints[0]
29
+ push_ep = config.endpoints[1]
30
+
31
+ @pull = OMQ::PULL.new(linger: config.linger, recv_timeout: config.timeout)
32
+ @push = OMQ::PUSH.new(linger: config.linger, send_timeout: config.timeout)
33
+ @pull.reconnect_interval = config.reconnect_ivl if config.reconnect_ivl
34
+ @push.reconnect_interval = config.reconnect_ivl if config.reconnect_ivl
35
+ @pull.heartbeat_interval = config.heartbeat_ivl if config.heartbeat_ivl
36
+ @push.heartbeat_interval = config.heartbeat_ivl if config.heartbeat_ivl
37
+
38
+ pull_ep.bind? ? @pull.bind(pull_ep.url) : @pull.connect(pull_ep.url)
39
+ push_ep.bind? ? @push.bind(push_ep.url) : @push.connect(push_ep.url)
40
+
41
+ compile_expr
42
+ @sock = @pull # for eval_expr instance_exec
43
+
44
+ with_timeout(config.timeout) do
45
+ @push.peer_connected.wait
46
+ @pull.peer_connected.wait
47
+ end
48
+
49
+ if config.transient
50
+ task.async do
51
+ @pull.all_peers_gone.wait
52
+ @pull.reconnect_enabled = false
53
+ @pull.close_read
54
+ end
55
+ end
56
+
57
+ @sock.instance_exec(&@begin_proc) if @begin_proc
58
+
59
+ n = config.count
60
+ i = 0
61
+ loop do
62
+ parts = @pull.receive
63
+ break if parts.nil?
64
+ parts = @fmt.decompress(parts)
65
+ parts = eval_expr(parts)
66
+ if parts && !parts.empty?
67
+ @push.send(@fmt.compress(parts))
68
+ end
69
+ i += 1
70
+ break if n && n > 0 && i >= n
71
+ end
72
+
73
+ @sock.instance_exec(&@end_proc) if @end_proc
74
+ ensure
75
+ @pull&.close
76
+ @push&.close
77
+ end
78
+
79
+
80
+ def run_parallel
81
+ workers = config.parallel.times.map do
82
+ Ractor.new(config) do |cfg|
83
+ $VERBOSE = nil
84
+ Console.logger = Console::Logger.new(Console::Output::Null.new)
85
+
86
+ Sync do |task|
87
+ # Parse BEGIN/END blocks and per-message expression
88
+ begin_proc = end_proc = eval_proc = nil
89
+ if cfg.expr
90
+ extract = ->(src, kw) {
91
+ s = src.index(/#{kw}\s*\{/)
92
+ return [src, nil] unless s
93
+ i = src.index("{", s); d = 1; j = i + 1
94
+ while j < src.length && d > 0
95
+ d += 1 if src[j] == "{"; d -= 1 if src[j] == "}"
96
+ j += 1
97
+ end
98
+ [src[0...s] + src[j..], src[(i + 1)..(j - 2)]]
99
+ }
100
+ expr, begin_body = extract.(cfg.expr, "BEGIN")
101
+ expr, end_body = extract.(expr, "END")
102
+ begin_proc = eval("proc { #{begin_body} }") if begin_body
103
+ end_proc = eval("proc { #{end_body} }") if end_body
104
+ if expr && !expr.strip.empty?
105
+ ractor_expr = expr.gsub(/\$F\b/, "__F")
106
+ eval_proc = eval("proc { |__F| $_ = __F&.first; #{ractor_expr} }")
107
+ end
108
+ end
109
+
110
+ formatter = OMQ::CLI::Formatter.new(cfg.format, compress: cfg.compress)
111
+
112
+ pull = OMQ::PULL.new(linger: cfg.linger, recv_timeout: cfg.timeout)
113
+ push = OMQ::PUSH.new(linger: cfg.linger, send_timeout: cfg.timeout)
114
+ pull.reconnect_interval = cfg.reconnect_ivl if cfg.reconnect_ivl
115
+ push.reconnect_interval = cfg.reconnect_ivl if cfg.reconnect_ivl
116
+ pull.heartbeat_interval = cfg.heartbeat_ivl if cfg.heartbeat_ivl
117
+ push.heartbeat_interval = cfg.heartbeat_ivl if cfg.heartbeat_ivl
118
+ pull.connect(cfg.endpoints[0].url)
119
+ push.connect(cfg.endpoints[1].url)
120
+
121
+ if cfg.timeout
122
+ task.with_timeout(cfg.timeout) do
123
+ push.peer_connected.wait
124
+ pull.peer_connected.wait
125
+ end
126
+ else
127
+ push.peer_connected.wait
128
+ pull.peer_connected.wait
129
+ end
130
+
131
+ if cfg.transient
132
+ task.async do
133
+ pull.all_peers_gone.wait
134
+ pull.reconnect_enabled = false
135
+ pull.close_read
136
+ end
137
+ end
138
+
139
+ begin_proc&.call
140
+
141
+ i = 0
142
+ loop do
143
+ parts = pull.receive
144
+ break if parts.nil?
145
+ parts = formatter.decompress(parts)
146
+ if eval_proc
147
+ result = eval_proc.call(parts)
148
+ parts = case result
149
+ when nil then nil
150
+ when Array then result
151
+ when String then [result]
152
+ else [result.to_s]
153
+ end
154
+ end
155
+ if parts && !parts.empty?
156
+ push.send(formatter.compress(parts))
157
+ end
158
+ i += 1
159
+ break if cfg.count && cfg.count > 0 && i >= cfg.count
160
+ end
161
+
162
+ end_proc&.call
163
+ rescue Async::TimeoutError
164
+ # exit cleanly on timeout
165
+ ensure
166
+ pull&.close
167
+ push&.close
168
+ end
169
+ end
170
+ end
171
+
172
+ workers.each do |w|
173
+ w.value
174
+ rescue Ractor::RemoteError => e
175
+ $stderr.puts "omq: Ractor error: #{e.cause&.message || e.message}"
176
+ end
177
+ end
178
+
179
+
180
+ def with_timeout(seconds)
181
+ if seconds
182
+ Async::Task.current.with_timeout(seconds) { yield }
183
+ else
184
+ yield
185
+ end
186
+ end
187
+
188
+
189
+ def compile_expr
190
+ return unless config.expr
191
+ expr, begin_body, end_body = extract_blocks(config.expr)
192
+ @begin_proc = eval("proc { #{begin_body} }") if begin_body
193
+ @end_proc = eval("proc { #{end_body} }") if end_body
194
+ @eval_proc = eval("proc { $_ = $F&.first; #{expr} }") if expr && !expr.strip.empty?
195
+ end
196
+
197
+
198
+ def extract_blocks(expr)
199
+ begin_body = end_body = nil
200
+ expr, begin_body = extract_block(expr, "BEGIN")
201
+ expr, end_body = extract_block(expr, "END")
202
+ [expr, begin_body, end_body]
203
+ end
204
+
205
+
206
+ def extract_block(expr, keyword)
207
+ start = expr.index(/#{keyword}\s*\{/)
208
+ return [expr, nil] unless start
209
+
210
+ i = expr.index("{", start)
211
+ depth = 1
212
+ j = i + 1
213
+ while j < expr.length && depth > 0
214
+ case expr[j]
215
+ when "{" then depth += 1
216
+ when "}" then depth -= 1
217
+ end
218
+ j += 1
219
+ end
220
+
221
+ body = expr[(i + 1)..(j - 2)]
222
+ trimmed = expr[0...start] + expr[j..]
223
+ [trimmed, body]
224
+ end
225
+
226
+
227
+ def eval_expr(parts)
228
+ return parts unless @eval_proc
229
+ $F = parts
230
+ result = @sock.instance_exec(&@eval_proc)
231
+ return nil if result.nil?
232
+ return [result] if config.format == :marshal
233
+ case result
234
+ when Array then result
235
+ when String then [result]
236
+ else [result.to_str]
237
+ end
238
+ rescue => e
239
+ $stderr.puts "omq: -e error: #{e.message} (#{e.class})"
240
+ Process.exit!(3)
241
+ end
242
+
243
+
244
+ def log(msg)
245
+ $stderr.puts(msg) if config.verbose
246
+ end
247
+ end
248
+ end
249
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OMQ
4
+ module CLI
5
+ class PubRunner < BaseRunner
6
+ def run_loop(task) = run_send_logic
7
+ end
8
+
9
+
10
+ class SubRunner < BaseRunner
11
+ def run_loop(task) = run_recv_logic
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OMQ
4
+ module CLI
5
+ class PushRunner < BaseRunner
6
+ def run_loop(task) = run_send_logic
7
+ end
8
+
9
+
10
+ class PullRunner < BaseRunner
11
+ def run_loop(task) = run_recv_logic
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OMQ
4
+ module CLI
5
+ class RadioRunner < BaseRunner
6
+ def run_loop(task) = run_send_logic
7
+
8
+
9
+ private
10
+
11
+
12
+ def send_msg(parts)
13
+ return if parts.empty?
14
+ parts = [Marshal.dump(parts)] if config.format == :marshal
15
+ parts = @fmt.compress(parts)
16
+ group = config.group || parts.shift
17
+ @sock.publish(group, parts.first || "")
18
+ transient_ready!
19
+ end
20
+ end
21
+
22
+
23
+ class DishRunner < BaseRunner
24
+ def run_loop(task) = run_recv_logic
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,77 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OMQ
4
+ module CLI
5
+ class ReqRunner < BaseRunner
6
+ private
7
+
8
+
9
+ def run_loop(task)
10
+ n = config.count
11
+ i = 0
12
+ sleep(config.delay) if config.delay
13
+ if config.interval
14
+ loop do
15
+ parts = read_next
16
+ break unless parts
17
+ send_msg(parts)
18
+ reply = recv_msg
19
+ break if reply.nil?
20
+ reply = eval_expr(reply)
21
+ output(reply)
22
+ i += 1
23
+ break if n && n > 0 && i >= n
24
+ interval = config.interval
25
+ wait = interval - (Time.now.to_f % interval)
26
+ sleep(wait) if wait > 0
27
+ end
28
+ else
29
+ loop do
30
+ parts = read_next
31
+ break unless parts
32
+ send_msg(parts)
33
+ reply = recv_msg
34
+ break if reply.nil?
35
+ reply = eval_expr(reply)
36
+ output(reply)
37
+ i += 1
38
+ break if n && n > 0 && i >= n
39
+ break if config.data || config.file
40
+ end
41
+ end
42
+ end
43
+ end
44
+
45
+
46
+ class RepRunner < BaseRunner
47
+ private
48
+
49
+
50
+ def run_loop(task)
51
+ n = config.count
52
+ i = 0
53
+ loop do
54
+ msg = recv_msg
55
+ break if msg.nil?
56
+ if config.expr
57
+ reply = eval_expr(msg)
58
+ output(reply)
59
+ send_msg(reply || [""])
60
+ elsif config.echo
61
+ output(msg)
62
+ send_msg(msg)
63
+ elsif config.data || config.file || !config.stdin_is_tty
64
+ reply = read_next
65
+ break unless reply
66
+ output(msg)
67
+ send_msg(reply)
68
+ else
69
+ abort "REP needs a reply source: --echo, --data, --file, -e, or stdin pipe"
70
+ end
71
+ i += 1
72
+ break if n && n > 0 && i >= n
73
+ end
74
+ end
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,70 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OMQ
4
+ module CLI
5
+ class DealerRunner < PairRunner
6
+ end
7
+
8
+
9
+ class RouterRunner < BaseRunner
10
+ private
11
+
12
+
13
+ def run_loop(task)
14
+ receiver = task.async do
15
+ n = config.count
16
+ i = 0
17
+ loop do
18
+ parts = recv_msg_raw
19
+ break if parts.nil?
20
+ identity = parts.shift
21
+ parts.shift if parts.first == ""
22
+ parts = @fmt.decompress(parts)
23
+ result = eval_expr([display_routing_id(identity), *parts])
24
+ output(result)
25
+ i += 1
26
+ break if n && n > 0 && i >= n
27
+ end
28
+ end
29
+
30
+ sender = task.async do
31
+ n = config.count
32
+ i = 0
33
+ sleep(config.delay) if config.delay
34
+ if config.interval
35
+ Async::Loop.quantized(interval: config.interval) do
36
+ parts = read_next
37
+ break unless parts
38
+ send_targeted_or_plain(parts)
39
+ i += 1
40
+ break if n && n > 0 && i >= n
41
+ end
42
+ elsif config.data || config.file
43
+ parts = read_next
44
+ send_targeted_or_plain(parts) if parts
45
+ else
46
+ loop do
47
+ parts = read_next
48
+ break unless parts
49
+ send_targeted_or_plain(parts)
50
+ i += 1
51
+ break if n && n > 0 && i >= n
52
+ end
53
+ end
54
+ end
55
+
56
+ wait_for_loops(receiver, sender)
57
+ end
58
+
59
+
60
+ def send_targeted_or_plain(parts)
61
+ if config.target
62
+ payload = @fmt.compress(parts)
63
+ @sock.send([resolve_target(config.target), "", *payload])
64
+ else
65
+ send_msg(parts)
66
+ end
67
+ end
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OMQ
4
+ module CLI
5
+ class ScatterRunner < BaseRunner
6
+ def run_loop(task) = run_send_logic
7
+ end
8
+
9
+
10
+ class GatherRunner < BaseRunner
11
+ def run_loop(task) = run_recv_logic
12
+ end
13
+ end
14
+ end