omq-rfc-clientserver 0.1.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 +15 -0
- data/README.md +39 -0
- data/lib/omq/client_server.rb +56 -0
- data/lib/omq/rfc/clientserver/version.rb +10 -0
- data/lib/omq/rfc/clientserver.rb +20 -0
- data/lib/omq/routing/client.rb +59 -0
- data/lib/omq/routing/server.rb +83 -0
- data/lib/omq/single_frame.rb +23 -0
- metadata +63 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: edfb06638148a82d92d092597322eecb30579a4ed9b89c8bfefdaa27517b14e0
|
|
4
|
+
data.tar.gz: 37576d85bc15c93d9c5d2afc8d77ea667c71dadfcfda1939f389046f75c08483
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 7c9782da703f80dbec0bb6a502057995af1a69569b2996406ed2108c5be1feb6cfe6fa86ddfc48e59ba9a595c04ec478a215aa2bcc8ad73209634231cf2adc73
|
|
7
|
+
data.tar.gz: c914053a277da5aba36981fedd821b5830fdb4b03a338c0cf2e033d0f515c8abe007d9f13d9d596d28097397879d8449d654a17484cdd1268f07609132abdd4d
|
data/LICENSE
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
ISC License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025-2026, Patrik Wenger
|
|
4
|
+
|
|
5
|
+
Permission to use, copy, modify, and/or distribute this software for any
|
|
6
|
+
purpose with or without fee is hereby granted, provided that the above
|
|
7
|
+
copyright notice and this permission notice appear in all copies.
|
|
8
|
+
|
|
9
|
+
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
|
10
|
+
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
|
11
|
+
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
|
12
|
+
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
|
13
|
+
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
|
14
|
+
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
|
15
|
+
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# OMQ::CLIENT and OMQ::SERVER
|
|
2
|
+
|
|
3
|
+
[](https://github.com/paddor/omq-rfc-clientserver/actions/workflows/ci.yml)
|
|
4
|
+
[](https://rubygems.org/gems/omq-rfc-clientserver)
|
|
5
|
+
[](LICENSE)
|
|
6
|
+
[](https://www.ruby-lang.org)
|
|
7
|
+
|
|
8
|
+
CLIENT and SERVER socket types ([RFC 41](https://rfc.zeromq.org/spec/41/))
|
|
9
|
+
for [OMQ](https://github.com/paddor/omq).
|
|
10
|
+
|
|
11
|
+
Single-frame, asynchronous request-reply. SERVER routes by 4-byte
|
|
12
|
+
connection ID; CLIENT round-robins.
|
|
13
|
+
|
|
14
|
+
## Usage
|
|
15
|
+
|
|
16
|
+
```ruby
|
|
17
|
+
require "omq"
|
|
18
|
+
require "omq/rfc/clientserver"
|
|
19
|
+
|
|
20
|
+
server = OMQ::SERVER.bind("tcp://127.0.0.1:5555")
|
|
21
|
+
client = OMQ::CLIENT.connect("tcp://127.0.0.1:5555")
|
|
22
|
+
|
|
23
|
+
client << "hello"
|
|
24
|
+
msg, routing_id = server.receive_with_routing_id
|
|
25
|
+
server.send_to(routing_id, msg.upcase)
|
|
26
|
+
reply = client.receive # => "HELLO"
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Installation
|
|
30
|
+
|
|
31
|
+
```ruby
|
|
32
|
+
gem "omq-rfc-clientserver"
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Requires `omq >= 0.12`.
|
|
36
|
+
|
|
37
|
+
## License
|
|
38
|
+
|
|
39
|
+
[ISC](LICENSE)
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module OMQ
|
|
4
|
+
# Asynchronous client socket for the CLIENT/SERVER pattern (ZeroMQ RFC 41).
|
|
5
|
+
#
|
|
6
|
+
# Round-robins outgoing messages across connected SERVER peers.
|
|
7
|
+
class CLIENT < Socket
|
|
8
|
+
include Readable
|
|
9
|
+
include Writable
|
|
10
|
+
include SingleFrame
|
|
11
|
+
|
|
12
|
+
# Creates a new CLIENT socket.
|
|
13
|
+
#
|
|
14
|
+
# @param endpoints [String, Array<String>, nil] endpoint(s) to connect to
|
|
15
|
+
# @param linger [Integer] linger period in milliseconds
|
|
16
|
+
# @param backend [Object, nil] optional transport backend
|
|
17
|
+
def initialize(endpoints = nil, linger: 0, backend: nil)
|
|
18
|
+
_init_engine(:CLIENT, linger: linger, backend: backend)
|
|
19
|
+
_attach(endpoints, default: :connect)
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
# Asynchronous server socket for the CLIENT/SERVER pattern (ZeroMQ RFC 41).
|
|
25
|
+
#
|
|
26
|
+
# Assigns a 4-byte routing ID to each connected CLIENT and supports
|
|
27
|
+
# directed replies via #send_to.
|
|
28
|
+
class SERVER < Socket
|
|
29
|
+
include Readable
|
|
30
|
+
include Writable
|
|
31
|
+
include SingleFrame
|
|
32
|
+
|
|
33
|
+
# Creates a new SERVER socket.
|
|
34
|
+
#
|
|
35
|
+
# @param endpoints [String, Array<String>, nil] endpoint(s) to bind to
|
|
36
|
+
# @param linger [Integer] linger period in milliseconds
|
|
37
|
+
# @param backend [Object, nil] optional transport backend
|
|
38
|
+
def initialize(endpoints = nil, linger: 0, backend: nil)
|
|
39
|
+
_init_engine(:SERVER, linger: linger, backend: backend)
|
|
40
|
+
_attach(endpoints, default: :bind)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
# Sends a message to a specific peer by routing ID.
|
|
45
|
+
#
|
|
46
|
+
# @param routing_id [String] 4-byte routing ID
|
|
47
|
+
# @param message [String] message body
|
|
48
|
+
# @return [self]
|
|
49
|
+
#
|
|
50
|
+
def send_to(routing_id, message)
|
|
51
|
+
parts = [routing_id.b.freeze, message.b.freeze]
|
|
52
|
+
with_timeout(@options.write_timeout) { @engine.enqueue_send(parts) }
|
|
53
|
+
self
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# OMQ CLIENT/SERVER socket types (ZeroMQ RFC 41).
|
|
4
|
+
#
|
|
5
|
+
# Usage:
|
|
6
|
+
# require "omq/rfc/clientserver"
|
|
7
|
+
#
|
|
8
|
+
# server = OMQ::SERVER.bind("tcp://127.0.0.1:5555")
|
|
9
|
+
# client = OMQ::CLIENT.connect("tcp://127.0.0.1:5555")
|
|
10
|
+
|
|
11
|
+
require "omq"
|
|
12
|
+
|
|
13
|
+
require_relative "clientserver/version"
|
|
14
|
+
require_relative "../single_frame"
|
|
15
|
+
require_relative "../routing/client"
|
|
16
|
+
require_relative "../routing/server"
|
|
17
|
+
require_relative "../client_server"
|
|
18
|
+
|
|
19
|
+
OMQ::Routing.register(:CLIENT, OMQ::Routing::Client)
|
|
20
|
+
OMQ::Routing.register(:SERVER, OMQ::Routing::Server)
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module OMQ
|
|
4
|
+
module Routing
|
|
5
|
+
# CLIENT socket routing: round-robin send, fair-queue receive.
|
|
6
|
+
#
|
|
7
|
+
# Same as DEALER — no envelope manipulation.
|
|
8
|
+
#
|
|
9
|
+
class Client
|
|
10
|
+
include RoundRobin
|
|
11
|
+
include FairRecv
|
|
12
|
+
|
|
13
|
+
# @param engine [Engine]
|
|
14
|
+
#
|
|
15
|
+
def initialize(engine)
|
|
16
|
+
@engine = engine
|
|
17
|
+
@recv_queue = FairQueue.new
|
|
18
|
+
@tasks = []
|
|
19
|
+
init_round_robin(engine)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
# @return [FairQueue]
|
|
24
|
+
#
|
|
25
|
+
attr_reader :recv_queue
|
|
26
|
+
|
|
27
|
+
# @param connection [Connection]
|
|
28
|
+
#
|
|
29
|
+
def connection_added(connection)
|
|
30
|
+
@connections << connection
|
|
31
|
+
add_fair_recv_connection(connection)
|
|
32
|
+
add_round_robin_send_connection(connection)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
# @param connection [Connection]
|
|
37
|
+
#
|
|
38
|
+
def connection_removed(connection)
|
|
39
|
+
@connections.delete(connection)
|
|
40
|
+
@recv_queue.remove_queue(connection)
|
|
41
|
+
remove_round_robin_send_connection(connection)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
# @param parts [Array<String>]
|
|
46
|
+
#
|
|
47
|
+
def enqueue(parts)
|
|
48
|
+
enqueue_round_robin(parts)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
# Stops all background tasks.
|
|
53
|
+
def stop
|
|
54
|
+
@tasks.each(&:stop)
|
|
55
|
+
@tasks.clear
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "securerandom"
|
|
4
|
+
|
|
5
|
+
module OMQ
|
|
6
|
+
module Routing
|
|
7
|
+
# SERVER socket routing: identity-based routing with auto-generated
|
|
8
|
+
# 4-byte routing IDs.
|
|
9
|
+
#
|
|
10
|
+
# Prepends routing ID on receive. Strips routing ID on send and
|
|
11
|
+
# routes to the identified connection.
|
|
12
|
+
#
|
|
13
|
+
class Server
|
|
14
|
+
include FairRecv
|
|
15
|
+
|
|
16
|
+
# @param engine [Engine]
|
|
17
|
+
#
|
|
18
|
+
def initialize(engine)
|
|
19
|
+
@engine = engine
|
|
20
|
+
@recv_queue = FairQueue.new
|
|
21
|
+
@connections_by_routing_id = {}
|
|
22
|
+
@routing_id_by_connection = {}
|
|
23
|
+
@conn_queues = {}
|
|
24
|
+
@conn_send_tasks = {}
|
|
25
|
+
@tasks = []
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
# @return [FairQueue]
|
|
30
|
+
#
|
|
31
|
+
attr_reader :recv_queue
|
|
32
|
+
|
|
33
|
+
# @param connection [Connection]
|
|
34
|
+
#
|
|
35
|
+
def connection_added(connection)
|
|
36
|
+
routing_id = SecureRandom.bytes(4)
|
|
37
|
+
@connections_by_routing_id[routing_id] = connection
|
|
38
|
+
@routing_id_by_connection[connection] = routing_id
|
|
39
|
+
|
|
40
|
+
add_fair_recv_connection(connection) { |msg| [routing_id, *msg] }
|
|
41
|
+
|
|
42
|
+
q = Routing.build_queue(@engine.options.send_hwm, :block)
|
|
43
|
+
@conn_queues[connection] = q
|
|
44
|
+
@conn_send_tasks[connection] = ConnSendPump.start(@engine, connection, q, @tasks)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
# @param connection [Connection]
|
|
49
|
+
#
|
|
50
|
+
def connection_removed(connection)
|
|
51
|
+
routing_id = @routing_id_by_connection.delete(connection)
|
|
52
|
+
@connections_by_routing_id.delete(routing_id) if routing_id
|
|
53
|
+
@recv_queue.remove_queue(connection)
|
|
54
|
+
@conn_queues.delete(connection)
|
|
55
|
+
@conn_send_tasks.delete(connection)&.stop
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
# @param parts [Array<String>]
|
|
60
|
+
#
|
|
61
|
+
def enqueue(parts)
|
|
62
|
+
routing_id = parts.first
|
|
63
|
+
conn = @connections_by_routing_id[routing_id]
|
|
64
|
+
return unless conn
|
|
65
|
+
@conn_queues[conn]&.enqueue(parts[1..])
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
# Stops all background tasks (send pumps).
|
|
70
|
+
def stop
|
|
71
|
+
@tasks.each(&:stop)
|
|
72
|
+
@tasks.clear
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
# True when all per-connection send queues are empty.
|
|
77
|
+
#
|
|
78
|
+
def send_queues_drained?
|
|
79
|
+
@conn_queues.values.all?(&:empty?)
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module OMQ
|
|
4
|
+
# Mixin that rejects multipart messages.
|
|
5
|
+
#
|
|
6
|
+
# All draft socket types (CLIENT, SERVER, RADIO, DISH, SCATTER,
|
|
7
|
+
# GATHER, PEER, CHANNEL) require single-frame messages for
|
|
8
|
+
# thread-safe atomic operations.
|
|
9
|
+
#
|
|
10
|
+
module SingleFrame
|
|
11
|
+
# Sends a message, rejecting multipart messages.
|
|
12
|
+
#
|
|
13
|
+
# @param message [String, Array<String>] message to send (must be single-frame)
|
|
14
|
+
# @raise [ArgumentError] if a multipart message is provided
|
|
15
|
+
# @return [void]
|
|
16
|
+
def send(message)
|
|
17
|
+
if message.is_a?(Array) && message.size > 1
|
|
18
|
+
raise ArgumentError, "#{self.class} does not support multipart messages"
|
|
19
|
+
end
|
|
20
|
+
super
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: omq-rfc-clientserver
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Patrik Wenger
|
|
8
|
+
bindir: bin
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: omq
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - ">="
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '0.12'
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - ">="
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '0.12'
|
|
26
|
+
description: CLIENT and SERVER socket types implementing ZeroMQ RFC 41 for the OMQ
|
|
27
|
+
pure-Ruby ZeroMQ library.
|
|
28
|
+
email:
|
|
29
|
+
- paddor@gmail.com
|
|
30
|
+
executables: []
|
|
31
|
+
extensions: []
|
|
32
|
+
extra_rdoc_files: []
|
|
33
|
+
files:
|
|
34
|
+
- LICENSE
|
|
35
|
+
- README.md
|
|
36
|
+
- lib/omq/client_server.rb
|
|
37
|
+
- lib/omq/rfc/clientserver.rb
|
|
38
|
+
- lib/omq/rfc/clientserver/version.rb
|
|
39
|
+
- lib/omq/routing/client.rb
|
|
40
|
+
- lib/omq/routing/server.rb
|
|
41
|
+
- lib/omq/single_frame.rb
|
|
42
|
+
homepage: https://github.com/paddor/omq-rfc41-clientserver
|
|
43
|
+
licenses:
|
|
44
|
+
- ISC
|
|
45
|
+
metadata: {}
|
|
46
|
+
rdoc_options: []
|
|
47
|
+
require_paths:
|
|
48
|
+
- lib
|
|
49
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
50
|
+
requirements:
|
|
51
|
+
- - ">="
|
|
52
|
+
- !ruby/object:Gem::Version
|
|
53
|
+
version: '3.3'
|
|
54
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
55
|
+
requirements:
|
|
56
|
+
- - ">="
|
|
57
|
+
- !ruby/object:Gem::Version
|
|
58
|
+
version: '0'
|
|
59
|
+
requirements: []
|
|
60
|
+
rubygems_version: 4.0.6
|
|
61
|
+
specification_version: 4
|
|
62
|
+
summary: ZMQ CLIENT/SERVER socket types (RFC 41) for OMQ
|
|
63
|
+
test_files: []
|