2020import java .util .HashMap ;
2121import java .util .HashSet ;
2222import java .util .Map ;
23+ import java .util .function .Consumer ;
2324import java .util .function .Supplier ;
2425
2526import org .zeromq .SocketType ;
2930import org .springframework .integration .channel .AbstractMessageChannel ;
3031import org .springframework .integration .mapping .BytesMessageMapper ;
3132import org .springframework .integration .support .json .EmbeddedJsonHeadersMessageMapper ;
33+ import org .springframework .integration .zeromq .ZeroMqProxy ;
3234import org .springframework .lang .Nullable ;
3335import org .springframework .messaging .Message ;
3436import org .springframework .messaging .MessageHandler ;
5557 * The {@link #setConnectUrl(String)} has to be as a standard ZeroMQ connect string, but with an extra port
5658 * over the colon - representing a frontend and backend sockets pair on ZeroMQ proxy.
5759 * For example: {@code tcp://localhost:6001:6002}.
58- * This way a sending and receiving operations on this channel are similar to interaction over a messaging broker.
60+ * Another option is to provide a reference to the {@link ZeroMqProxy} instance managed in the same application:
61+ * frontend and backend ports are evaluated from this proxy and respective connection string is built from them.
62+ * <p>
63+ * This way sending and receiving operations on this channel are similar to interaction over a messaging broker.
5964 * <p>
6065 * An internal logic of this message channel implementation is based on the project Reactor using its
6166 * {@link Mono}, {@link Flux} and {@link Scheduler} API for better thead model and flow control to avoid
6772 */
6873public class ZeroMqChannel extends AbstractMessageChannel implements SubscribableChannel {
6974
75+ public static final Duration DEFAULT_CONSUME_DELAY = Duration .ofSeconds (1 );
76+
7077 private final Map <MessageHandler , Disposable > subscribers = new HashMap <>();
7178
7279 private final Scheduler publisherScheduler = Schedulers .newSingle ("publisherScheduler" );
@@ -83,8 +90,17 @@ public class ZeroMqChannel extends AbstractMessageChannel implements Subscribabl
8390
8491 private final Flux <? extends Message <?>> subscriberData ;
8592
93+ private Duration consumeDelay = DEFAULT_CONSUME_DELAY ;
94+
8695 private BytesMessageMapper messageMapper = new EmbeddedJsonHeadersMessageMapper ();
8796
97+ private Consumer <ZMQ .Socket > sendSocketConfigurer = (socket ) -> { };
98+
99+ private Consumer <ZMQ .Socket > subscribeSocketConfigurer = (socket ) -> { };
100+
101+ @ Nullable
102+ private ZeroMqProxy zeroMqProxy ;
103+
88104 @ Nullable
89105 private volatile String connectSendUrl ;
90106
@@ -107,30 +123,51 @@ public ZeroMqChannel(ZContext context, boolean pubSub) {
107123
108124 Supplier <String > localPairConnection = () -> "inproc://" + getComponentName () + ".pair" ;
109125
126+ Mono <?> proxyMono =
127+ Mono .defer (() -> {
128+ if (this .zeroMqProxy != null ) {
129+ return Mono .just (this .zeroMqProxy .getBackendPort ())
130+ .filter ((port ) -> port > 0 )
131+ .repeatWhenEmpty ((repeat ) -> repeat .delayElements (Duration .ofMillis (100 ))) // NOSONAR
132+ .doOnNext ((port ) ->
133+ setConnectUrl ("tcp://localhost:" + this .zeroMqProxy .getFrontendPort () +
134+ ':' + this .zeroMqProxy .getBackendPort ()));
135+ }
136+ else {
137+ return Mono .empty ();
138+ }
139+ })
140+ .cache ();
141+
110142 this .sendSocket =
111- Mono .fromCallable (() ->
112- this .context .createSocket (
113- this .connectSendUrl == null
114- ? SocketType .PAIR
115- : (this .pubSub ? SocketType .XPUB : SocketType .PUSH ))
116- )
143+ proxyMono
117144 .publishOn (this .publisherScheduler )
145+ .then (Mono .fromCallable (() ->
146+ this .context .createSocket (
147+ this .connectSendUrl == null
148+ ? SocketType .PAIR
149+ : (this .pubSub ? SocketType .XPUB : SocketType .PUSH ))
150+ ))
151+ .doOnNext (this .sendSocketConfigurer )
118152 .doOnNext ((socket ) ->
119153 socket .connect (this .connectSendUrl != null
120154 ? this .connectSendUrl
121155 : localPairConnection .get ()))
122- .delayUntil ((socket ) -> (this .pubSub && this .connectSendUrl != null )
123- ? Mono .just (socket ).map (ZMQ .Socket ::recv )
124- : Mono .empty ())
156+ .delayUntil ((socket ) ->
157+ (this .pubSub && this .connectSendUrl != null )
158+ ? Mono .just (socket ).map (ZMQ .Socket ::recv )
159+ : Mono .empty ())
125160 .cache ();
126161
127162 this .subscribeSocket =
128- Mono .fromCallable (() ->
129- this .context .createSocket (
130- this .connectSubscribeUrl == null
131- ? SocketType .PAIR
132- : (this .pubSub ? SocketType .SUB : SocketType .PULL )))
163+ proxyMono
133164 .publishOn (this .subscriberScheduler )
165+ .then (Mono .fromCallable (() ->
166+ this .context .createSocket (
167+ this .connectSubscribeUrl == null
168+ ? SocketType .PAIR
169+ : (this .pubSub ? SocketType .SUB : SocketType .PULL ))))
170+ .doOnNext (this .subscribeSocketConfigurer )
134171 .doOnNext ((socket ) -> {
135172 if (this .connectSubscribeUrl != null ) {
136173 socket .connect (this .connectSubscribeUrl );
@@ -160,7 +197,7 @@ public ZeroMqChannel(ZContext context, boolean pubSub) {
160197 .doOnError ((error ) -> logger .error ("Error processing ZeroMQ message" , error ))
161198 .repeatWhenEmpty ((repeat ) ->
162199 this .initialized
163- ? repeat .delayElements (Duration . ofMillis ( 100 ) )
200+ ? repeat .delayElements (this . consumeDelay )
164201 : repeat )
165202 .repeat (() -> this .initialized );
166203
@@ -173,6 +210,12 @@ public ZeroMqChannel(ZContext context, boolean pubSub) {
173210
174211 }
175212
213+ /**
214+ * Configure a connection to the ZeroMQ proxy with the pair of ports over colon
215+ * for proxy frontend and backend sockets. Mutually exclusive with the {@link #setZeroMqProxy(ZeroMqProxy)}.
216+ * @param connectUrl the connection string in format {@code PROTOCOL://HOST:FRONTEND_PORT:BACKEND_PORT},
217+ * e.g. {@code tcp://localhost:6001:6002}
218+ */
176219 public void setConnectUrl (@ Nullable String connectUrl ) {
177220 if (connectUrl != null ) {
178221 this .connectSendUrl = connectUrl .substring (0 , connectUrl .lastIndexOf (':' ));
@@ -182,13 +225,40 @@ public void setConnectUrl(@Nullable String connectUrl) {
182225 }
183226 }
184227
228+ /**
229+ * Specify a reference to a {@link ZeroMqProxy} instance in the same application
230+ * to rely on its ports configuration and make a natural lifecycle dependency without guessing
231+ * when the proxy is started. Mutually exclusive with the {@link #setConnectUrl(String)}.
232+ * @param zeroMqProxy the {@link ZeroMqProxy} instance to use
233+ */
234+ public void setZeroMqProxy (@ Nullable ZeroMqProxy zeroMqProxy ) {
235+ this .zeroMqProxy = zeroMqProxy ;
236+ }
237+
238+ public void setConsumeDelay (Duration consumeDelay ) {
239+ Assert .notNull (consumeDelay , "'consumeDelay' must not be null" );
240+ this .consumeDelay = consumeDelay ;
241+ }
242+
185243 public void setMessageMapper (BytesMessageMapper messageMapper ) {
186244 Assert .notNull (messageMapper , "'messageMapper' must not be null" );
187245 this .messageMapper = messageMapper ;
188246 }
189247
248+ public void setSendSocketConfigurer (Consumer <ZMQ .Socket > sendSocketConfigurer ) {
249+ Assert .notNull (sendSocketConfigurer , "'sendSocketConfigurer' must not be null" );
250+ this .sendSocketConfigurer = sendSocketConfigurer ;
251+ }
252+
253+ public void setSubscribeSocketConfigurer (Consumer <ZMQ .Socket > subscribeSocketConfigurer ) {
254+ Assert .notNull (subscribeSocketConfigurer , "'subscribeSocketConfigurer' must not be null" );
255+ this .subscribeSocketConfigurer = subscribeSocketConfigurer ;
256+ }
257+
190258 @ Override
191259 protected void onInit () {
260+ Assert .state (this .zeroMqProxy == null || this .connectSendUrl == null ,
261+ "Or 'zeroMqProxy' or 'connectUrl' can be provided (or none), but not both." );
192262 super .onInit ();
193263 this .sendSocket .subscribe ();
194264 this .initialized = true ;
0 commit comments