Skip to content

Commit 7201471

Browse files
authored
Merge pull request #558 from avinxshKD/julia-zmq-backend
concore.jl: add optional ZMQ backend
2 parents cdd1c9f + 134e3f1 commit 7201471

3 files changed

Lines changed: 348 additions & 1 deletion

File tree

concore.jl

Lines changed: 239 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,19 @@
99
# Uses relative paths (./in, ./out) for local execution.
1010
# For Docker containers, use concoredocker.jl instead.
1111
#
12-
# No external dependencies -- only Julia stdlib.
12+
# No hard external dependencies; ZMQ.jl is optional.
1313

1414
module Concore
1515

1616
using Mmap
1717

18+
const HAS_ZMQ = try
19+
@eval import ZMQ
20+
true
21+
catch
22+
false
23+
end
24+
1825
# -----------------------------------------------------------------------------
1926
# Backend selection
2027
# -----------------------------------------------------------------------------
@@ -35,9 +42,13 @@ struct MmapBackend <: AbstractBackend
3542
end
3643
end
3744

45+
"""ZeroMQ transport marker backend."""
46+
struct ZmqBackend <: AbstractBackend end
47+
3848
# Compatibility name for proposal/docs wording.
3949
const FileTransport = FileBackend
4050
const MmapTransport = MmapBackend
51+
const ZmqTransport = ZmqBackend
4152

4253
# -----------------------------------------------------------------------------
4354
# Path configuration
@@ -97,10 +108,189 @@ _backend_inpath(::FileBackend) = inpath
97108
_backend_outpath(::FileBackend) = outpath
98109
_backend_inpath(::MmapBackend) = inpath
99110
_backend_outpath(::MmapBackend) = outpath
111+
_backend_inpath(::ZmqBackend) = "zmq://in"
112+
_backend_outpath(::ZmqBackend) = "zmq://out"
100113

101114
_input_dir(port::Int) = _backend_inpath(_backend) * string(port)
102115
_output_dir(port::Int) = _backend_outpath(_backend) * string(port)
103116

117+
mutable struct ZeroMQPort
118+
context::Any
119+
socket::Any
120+
port_type::String
121+
address::String
122+
socket_type::String
123+
end
124+
125+
const zmq_ports = Dict{String, ZeroMQPort}()
126+
const _zmq_context = Ref{Any}(nothing)
127+
const _zmq_cleanup_registered = Ref(false)
128+
129+
function _require_zmq()
130+
HAS_ZMQ && return nothing
131+
error("ZMQ.jl is not installed. Install it with: using Pkg; Pkg.add(\"ZMQ\")")
132+
end
133+
134+
function _zmq_socket_type(socket_type::AbstractString)
135+
_require_zmq()
136+
upper = uppercase(String(socket_type))
137+
for name in ("REQ", "REP", "PUB", "SUB", "PUSH", "PULL", "PAIR")
138+
upper == name && return getfield(ZMQ, Symbol(name))
139+
end
140+
error("unknown ZMQ socket type '$socket_type'")
141+
end
142+
143+
function _register_zmq_cleanup()
144+
if !_zmq_cleanup_registered[]
145+
atexit(terminate_zmq)
146+
_zmq_cleanup_registered[] = true
147+
end
148+
end
149+
150+
function _get_zmq_context()
151+
_require_zmq()
152+
if _zmq_context[] === nothing
153+
_zmq_context[] = ZMQ.Context()
154+
end
155+
return _zmq_context[]
156+
end
157+
158+
function _configure_zmq_socket(socket)
159+
ZMQ.set_rcvtimeo(socket, 2000)
160+
ZMQ.set_sndtimeo(socket, 2000)
161+
ZMQ.set_linger(socket, 0)
162+
return nothing
163+
end
164+
165+
function _zmq_send(socket, payload::AbstractString; max_retries::Int = 5)
166+
_require_zmq()
167+
max_retries > 0 || throw(ArgumentError("max_retries must be positive"))
168+
for attempt in 1:max_retries
169+
try
170+
ZMQ.send(socket, String(payload))
171+
return nothing
172+
catch
173+
attempt == max_retries && rethrow()
174+
sleep(0.5)
175+
end
176+
end
177+
end
178+
179+
function _zmq_recv(socket; max_retries::Int = 5)::String
180+
_require_zmq()
181+
max_retries > 0 || throw(ArgumentError("max_retries must be positive"))
182+
for attempt in 1:max_retries
183+
try
184+
return String(ZMQ.recv(socket))
185+
catch
186+
attempt == max_retries && rethrow()
187+
sleep(0.5)
188+
end
189+
end
190+
error("ZMQ receive failed")
191+
end
192+
193+
function _zmq_payload(val::AbstractVector{<:Real}, delta::Real)::String
194+
return _format_wire(Float64[simtime + Float64(delta); Float64.(val)])
195+
end
196+
197+
function _zmq_payload(val::AbstractString, delta::Real)::String
198+
return String(val)
199+
end
200+
201+
function _close_zmq_port(port::ZeroMQPort)
202+
try
203+
ZMQ.close(port.socket)
204+
catch
205+
end
206+
return nothing
207+
end
208+
209+
function init_zmq_port(
210+
port_name::AbstractString,
211+
port_type::AbstractString,
212+
address::AbstractString,
213+
socket_type::AbstractString,
214+
)
215+
_require_zmq()
216+
_register_zmq_cleanup()
217+
218+
ptype = lowercase(String(port_type))
219+
(ptype == "bind" || ptype == "connect") || error("port_type must be bind or connect")
220+
221+
if haskey(zmq_ports, String(port_name))
222+
return nothing
223+
end
224+
225+
ctx = _get_zmq_context()
226+
socket = ZMQ.Socket(ctx, _zmq_socket_type(socket_type))
227+
_configure_zmq_socket(socket)
228+
229+
if ptype == "bind"
230+
ZMQ.bind(socket, String(address))
231+
else
232+
ZMQ.connect(socket, String(address))
233+
end
234+
235+
zmq_ports[String(port_name)] = ZeroMQPort(
236+
ctx,
237+
socket,
238+
ptype,
239+
String(address),
240+
uppercase(String(socket_type)),
241+
)
242+
return nothing
243+
end
244+
245+
function init_zmq_port(port_name, port_type::Symbol, address, socket_type::Symbol)
246+
init_zmq_port(string(port_name), string(port_type), string(address), string(socket_type))
247+
end
248+
249+
function _zmq_read(port_name::AbstractString, initstr::AbstractString)::Vector{Float64}
250+
_require_zmq()
251+
port = get(zmq_ports, String(port_name), nothing)
252+
port === nothing && error("ZMQ port '$port_name' not registered")
253+
254+
global simtime
255+
try
256+
msg = _zmq_recv(port.socket)
257+
vals = safe_parse_list(msg)
258+
simtime = max(simtime, vals[1])
259+
return vals[2:end]
260+
catch
261+
return initval(initstr)
262+
end
263+
end
264+
265+
function _zmq_write(
266+
port_name::AbstractString,
267+
val::Union{AbstractVector{<:Real},AbstractString},
268+
delta::Real,
269+
)
270+
_require_zmq()
271+
port = get(zmq_ports, String(port_name), nothing)
272+
port === nothing && error("ZMQ port '$port_name' not registered")
273+
274+
payload = _zmq_payload(val, delta)
275+
_zmq_send(port.socket, payload)
276+
return nothing
277+
end
278+
279+
function terminate_zmq()
280+
for port in values(zmq_ports)
281+
_close_zmq_port(port)
282+
end
283+
empty!(zmq_ports)
284+
if _zmq_context[] !== nothing
285+
try
286+
ZMQ.close(_zmq_context[])
287+
catch
288+
end
289+
_zmq_context[] = nothing
290+
end
291+
return nothing
292+
end
293+
104294
const _mmap_segments = Dict{String, Tuple{IOStream, Vector{UInt8}}}()
105295
const _mmap_cleanup_registered = Ref(false)
106296

@@ -498,6 +688,21 @@ function concore_read(
498688
return val[2:end]
499689
end
500690

691+
function concore_read(
692+
port_identifier::AbstractString,
693+
name::AbstractString,
694+
initstr::AbstractString,
695+
)::Vector{Float64}
696+
if haskey(zmq_ports, String(port_identifier))
697+
return _zmq_read(port_identifier, initstr)
698+
end
699+
700+
file_port = tryparse(Int, port_identifier)
701+
file_port !== nothing && return concore_read(file_port, name, initstr)
702+
703+
error("ZMQ port '$port_identifier' not registered")
704+
end
705+
501706
"""
502707
concore_write(port::Int, name::AbstractString, val::AbstractVector{<:Real}; delta::Real=0)
503708
@@ -525,6 +730,22 @@ function concore_write(
525730
return nothing
526731
end
527732

733+
function concore_write(
734+
port_identifier::AbstractString,
735+
name::AbstractString,
736+
val::AbstractVector{<:Real};
737+
delta::Real = 0,
738+
)
739+
if haskey(zmq_ports, String(port_identifier))
740+
return _zmq_write(port_identifier, val, delta)
741+
end
742+
743+
file_port = tryparse(Int, port_identifier)
744+
file_port !== nothing && return concore_write(file_port, name, val; delta=delta)
745+
746+
error("ZMQ port '$port_identifier' not registered")
747+
end
748+
528749
"""
529750
concore_write(port::Int, name::AbstractString, val::AbstractString; delta::Int=0)
530751
@@ -547,6 +768,22 @@ function concore_write(
547768
return nothing
548769
end
549770

771+
function concore_write(
772+
port_identifier::AbstractString,
773+
name::AbstractString,
774+
val::AbstractString;
775+
delta::Int = 0,
776+
)
777+
if haskey(zmq_ports, String(port_identifier))
778+
return _zmq_write(port_identifier, val, delta)
779+
end
780+
781+
file_port = tryparse(Int, port_identifier)
782+
file_port !== nothing && return concore_write(file_port, name, val; delta=delta)
783+
784+
error("ZMQ port '$port_identifier' not registered")
785+
end
786+
550787
"""
551788
initval(simtime_val::AbstractString) -> Vector{Float64}
552789
@@ -617,6 +854,7 @@ export load_iport!, load_oport!, load_params!, concore_init!
617854
export load_iport, load_oport, load_params, default_maxtime, concore_init
618855
export AbstractBackend, FileBackend, FileTransport
619856
export MmapBackend, MmapTransport, mmap_cleanup
857+
export ZmqBackend, ZmqTransport, HAS_ZMQ, init_zmq_port, terminate_zmq, zmq_ports
620858

621859
# -----------------------------------------------------------------------------
622860
# Auto-initialize on load

tests/julia/runtests.jl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,4 +11,5 @@ using .Concore
1111
include("test_wire_compat.jl")
1212
include("test_interop.jl")
1313
include("test_mmap.jl")
14+
include("test_zmq.jl")
1415
end

0 commit comments

Comments
 (0)