All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.springframework.integration.zeromq.ZeroMqProxy Maven / Gradle / Ivy

There is a newer version: 6.4.1
Show newest version
/*
 * Copyright 2020-2023 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.springframework.integration.zeromq;

import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.function.Consumer;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.zeromq.SocketType;
import org.zeromq.ZContext;
import org.zeromq.ZMQ;

import org.springframework.beans.factory.BeanNameAware;
import org.springframework.beans.factory.DisposableBean;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.context.SmartLifecycle;
import org.springframework.core.task.SimpleAsyncTaskExecutor;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;

/**
 * This class encapsulates the logic to configure and manage a ZeroMQ proxy.
 * It binds frontend and backend sockets over TCP on all the available network interfaces
 * with either provided or randomly selected ports.
 * 

* The {@link ZeroMqProxy.Type} dictates which pair of ZeroMQ sockets to bind with this proxy * to implement any possible patterns for ZeroMQ intermediary. Defaults to @link {@link ZeroMqProxy.Type#PULL_PUSH}. *

* The control socket is exposed as a {@link SocketType#PAIR} with an inter-thread transport * on the {@code "inproc://" + beanName + ".control"} address; it can be obtained via {@link #getControlAddress()}. * Should be used with the same application from {@link SocketType#PAIR} socket to send * {@link zmq.ZMQ#PROXY_TERMINATE}, {@link zmq.ZMQ#PROXY_PAUSE} and/or {@link zmq.ZMQ#PROXY_RESUME} commands. *

* If the proxy cannot be started for some reason, an error message is logged and this component is * left in the non-started state. *

* With an {@link #exposeCaptureSocket} option, an additional capture data socket is bound to inter-thread transport * as a {@link SocketType#PUB}. There is no specific topic selection, so all the subscribers to this socket * must subscribe with plain {@link ZMQ#SUBSCRIPTION_ALL}. * The address for this socket is {@code "inproc://" + beanName + ".capture"}. * * @author Artem Bilan * @author Christian Tzolov * * @since 5.4 * * @see ZMQ#proxy(ZMQ.Socket, ZMQ.Socket, ZMQ.Socket) */ public class ZeroMqProxy implements InitializingBean, SmartLifecycle, BeanNameAware, DisposableBean { private static final Log LOG = LogFactory.getLog(ZeroMqProxy.class); private final Lock lock = new ReentrantLock(); private final ZContext context; private final Type type; private final AtomicBoolean running = new AtomicBoolean(); private final AtomicInteger frontendPort = new AtomicInteger(); private final AtomicInteger backendPort = new AtomicInteger(); private String controlAddress; private Executor proxyExecutor; @Nullable private Consumer frontendSocketConfigurer; @Nullable private Consumer backendSocketConfigurer; private boolean exposeCaptureSocket; @Nullable private String captureAddress; private String beanName; private boolean autoStartup = true; private int phase; /** * Create a {@link ZeroMqProxy} instance based on the provided {@link ZContext} * and {@link Type#PULL_PUSH} as default mode. * @param context the {@link ZContext} to use */ public ZeroMqProxy(ZContext context) { this(context, Type.PULL_PUSH); } /** * Create a {@link ZeroMqProxy} instance based on the provided {@link ZContext} * and {@link Type}. * @param context the {@link ZContext} to use * @param type the {@link Type} to use. */ public ZeroMqProxy(ZContext context, Type type) { Assert.notNull(context, "'context' must not be null"); Assert.notNull(type, "'type' must not be null"); this.context = context; this.type = type; } /** * Configure an executor to perform a ZeroMQ proxy loop. * The thread is held until ZeroMQ proxy loop is terminated. * By default, an internal {@link Executors#newSingleThreadExecutor} instance is used. * @param proxyExecutor the {@link Executor} to use for ZeroMQ proxy loop */ public void setProxyExecutor(Executor proxyExecutor) { Assert.notNull(proxyExecutor, "'proxyExecutor' must not be null"); this.proxyExecutor = proxyExecutor; } /** * Specify a fixed port for frontend socket of the proxy. * @param frontendPort the port to use; must be more than 0 */ public void setFrontendPort(int frontendPort) { Assert.isTrue(frontendPort > 0, "'frontendPort' must not be zero or negative"); this.frontendPort.set(frontendPort); } /** * Specify a fixed port for backend socket of the proxy. * @param backendPort the port to use; must be more than 0 */ public void setBackendPort(int backendPort) { Assert.isTrue(backendPort > 0, "'backendPort' must not be zero or negative"); this.backendPort.set(backendPort); } /** * Provide a {@link Consumer} to configure a proxy frontend socket with arbitrary options, like security. * @param frontendSocketConfigurer the configurer for frontend socket */ public void setFrontendSocketConfigurer(@Nullable Consumer frontendSocketConfigurer) { this.frontendSocketConfigurer = frontendSocketConfigurer; } /** * Provide a {@link Consumer} to configure a proxy backend socket with arbitrary options, like security. * @param backendSocketConfigurer the configurer for backend socket */ public void setBackendSocketConfigurer(@Nullable Consumer backendSocketConfigurer) { this.backendSocketConfigurer = backendSocketConfigurer; } /** * Whether to bind and expose a capture socket for the proxy data. * @param exposeCaptureSocket true to bind capture socket for proxy */ public void setExposeCaptureSocket(boolean exposeCaptureSocket) { this.exposeCaptureSocket = exposeCaptureSocket; } @Override public void setBeanName(String beanName) { this.beanName = beanName; } public void setAutoStartup(boolean autoStartup) { this.autoStartup = autoStartup; } public void setPhase(int phase) { this.phase = phase; } public Type getType() { return this.type; } /** * Return the port a frontend socket is bound or 0 if this proxy has not been started yet. * @return the port for a frontend socket or 0 */ public int getFrontendPort() { return this.frontendPort.get(); } /** * Return the port a backend socket is bound or null if this proxy has not been started yet. * @return the port for a backend socket or 0 */ public int getBackendPort() { return this.backendPort.get(); } /** * Return the address an {@code inproc} control socket is bound or null if this proxy has not been started yet. * @return the address for control socket or null */ @Nullable public String getControlAddress() { return this.controlAddress; } /** * Return the address an {@code inproc} capture socket is bound or null if this proxy has not been started yet * or {@link #captureAddress} is false. * @return the address for capture socket or null */ @Nullable public String getCaptureAddress() { return this.captureAddress; } @Override public boolean isAutoStartup() { return this.autoStartup; } @Override public int getPhase() { return this.phase; } @Override public void afterPropertiesSet() { if (this.proxyExecutor == null) { this.proxyExecutor = new SimpleAsyncTaskExecutor(this.beanName + "-"); } this.controlAddress = "inproc://" + this.beanName + ".control"; if (this.exposeCaptureSocket) { this.captureAddress = "inproc://" + this.beanName + ".capture"; } } @Override public void start() { this.lock.lock(); try { if (!this.running.get()) { this.proxyExecutor .execute(() -> { ZMQ.Socket captureSocket = null; if (this.exposeCaptureSocket) { captureSocket = this.context.createSocket(SocketType.PUB); } try ( ZMQ.Socket frontendSocket = this.context .createSocket(this.type.getFrontendSocketType()); ZMQ.Socket backendSocket = this.context .createSocket(this.type.getBackendSocketType()); ZMQ.Socket controlSocket = this.context.createSocket(SocketType.PAIR)) { if (this.frontendSocketConfigurer != null) { this.frontendSocketConfigurer.accept(frontendSocket); } if (this.backendSocketConfigurer != null) { this.backendSocketConfigurer.accept(backendSocket); } this.frontendPort.set(bindSocket(frontendSocket, this.frontendPort.get())); // NOSONAR this.backendPort.set(bindSocket(backendSocket, this.backendPort.get())); // NOSONAR boolean bound = controlSocket.bind(this.controlAddress); // NOSONAR if (!bound) { throw new IllegalArgumentException("Cannot bind ZeroMQ socket to address: " + this.controlAddress); } if (captureSocket != null) { bound = captureSocket.bind(this.captureAddress); if (!bound) { throw new IllegalArgumentException("Cannot bind ZeroMQ socket to address: " + this.captureAddress); } } this.running.set(true); ZMQ.proxy(frontendSocket, backendSocket, captureSocket, controlSocket); } catch (Exception ex) { // NOSONAR LOG.error("Cannot start ZeroMQ proxy from bean: " + this.beanName, ex); } finally { if (captureSocket != null) { captureSocket.close(); } } }); } } finally { this.lock.unlock(); } } @Override public void stop() { this.lock.lock(); try { if (this.running.getAndSet(false)) { try (ZMQ.Socket commandSocket = this.context.createSocket(SocketType.PAIR)) { commandSocket.connect(this.controlAddress); // NOSONAR commandSocket.send(zmq.ZMQ.PROXY_TERMINATE); } } } finally { this.lock.unlock(); } } @Override public boolean isRunning() { return this.running.get(); } @Override public void destroy() { stop(); } private static int bindSocket(ZMQ.Socket socket, int port) { if (port == 0) { return socket.bindToRandomPort("tcp://*"); } else { boolean bound = socket.bind("tcp://*:" + port); if (!bound) { throw new IllegalArgumentException("Cannot bind ZeroMQ socket to port: " + port); } return port; } } public enum Type { SUB_PUB(SocketType.XSUB, SocketType.XPUB), PULL_PUSH(SocketType.PULL, SocketType.PUSH), ROUTER_DEALER(SocketType.ROUTER, SocketType.DEALER); private final SocketType frontendSocketType; private final SocketType backendSocketType; Type(SocketType frontendSocketType, SocketType backendSocketType) { this.frontendSocketType = frontendSocketType; this.backendSocketType = backendSocketType; } public SocketType getFrontendSocketType() { return this.frontendSocketType; } public SocketType getBackendSocketType() { return this.backendSocketType; } } }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy