2 * ============LICENSE_START=======================================================
4 * ================================================================================
5 * Copyright (C) 2020-2021 AT&T Intellectual Property. All rights reserved.
6 * Modifications Copyright (C) 2023 Nordix Foundation.
7 * ================================================================================
8 * Licensed under the Apache License, Version 2.0 (the "License");
9 * you may not use this file except in compliance with the License.
10 * You may obtain a copy of the License at
12 * http://www.apache.org/licenses/LICENSE-2.0
14 * Unless required by applicable law or agreed to in writing, software
15 * distributed under the License is distributed on an "AS IS" BASIS,
16 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17 * See the License for the specific language governing permissions and
18 * limitations under the License.
19 * ============LICENSE_END=========================================================
22 package org.onap.policy.common.endpoints.event.comm.client;
24 import java.util.Arrays;
25 import java.util.Collections;
26 import java.util.List;
27 import java.util.concurrent.BlockingDeque;
28 import java.util.concurrent.LinkedBlockingDeque;
29 import java.util.concurrent.TimeUnit;
31 import org.jetbrains.annotations.NotNull;
32 import org.onap.policy.common.endpoints.event.comm.Topic.CommInfrastructure;
33 import org.onap.policy.common.endpoints.event.comm.TopicEndpoint;
34 import org.onap.policy.common.endpoints.event.comm.TopicEndpointManager;
35 import org.onap.policy.common.endpoints.event.comm.TopicListener;
36 import org.onap.policy.common.endpoints.event.comm.TopicSink;
37 import org.onap.policy.common.endpoints.event.comm.TopicSource;
38 import org.onap.policy.common.utils.coder.Coder;
39 import org.onap.policy.common.utils.coder.CoderException;
40 import org.onap.policy.common.utils.coder.StandardCoder;
41 import org.slf4j.Logger;
42 import org.slf4j.LoggerFactory;
45 * A "bidirectional" topic, which is a pair of topics, one of which is used to publish
46 * requests and the other to receive responses.
49 public class BidirectionalTopicClient {
50 private static final Logger logger = LoggerFactory.getLogger(BidirectionalTopicClient.class);
51 private static final Coder coder = new StandardCoder();
53 private final String sinkTopic;
54 private final String sourceTopic;
55 private final TopicSink sink;
56 private final TopicSource source;
57 private final CommInfrastructure sinkTopicCommInfrastructure;
58 private final CommInfrastructure sourceTopicCommInfrastructure;
61 * Used when checking whether a message sent on the sink topic can be received
62 * on the source topic. When a matching message is received on the incoming topic,
63 * {@code true} is placed on the queue. If {@link #stopWaiting()} is called or the waiting
64 * thread is interrupted, then {@code false} is placed on the queue. Whenever a value
65 * is pulled from the queue, it is immediately placed back on the queue.
67 private final BlockingDeque<Boolean> checkerQueue = new LinkedBlockingDeque<>();
71 * Constructs the object.
73 * @param sinkTopic sink topic name
74 * @param sourceTopic source topic name
75 * @throws BidirectionalTopicClientException if either topic does not exist
77 public BidirectionalTopicClient(String sinkTopic, String sourceTopic) throws BidirectionalTopicClientException {
78 this.sinkTopic = sinkTopic.toLowerCase();
79 this.sourceTopic = sourceTopic.toLowerCase();
82 List<TopicSink> sinks = getTopicEndpointManager().getTopicSinks(sinkTopic);
83 if (sinks.isEmpty()) {
84 throw new BidirectionalTopicClientException("no sinks for topic: " + sinkTopic);
85 } else if (sinks.size() > 1) {
86 throw new BidirectionalTopicClientException("too many sinks for topic: " + sinkTopic);
89 this.sink = sinks.get(0);
92 List<TopicSource> sources = getTopicEndpointManager().getTopicSources(Collections.singletonList(sourceTopic));
93 if (sources.isEmpty()) {
94 throw new BidirectionalTopicClientException("no sources for topic: " + sourceTopic);
95 } else if (sources.size() > 1) {
96 throw new BidirectionalTopicClientException("too many sources for topic: " + sourceTopic);
99 this.source = sources.get(0);
101 this.sinkTopicCommInfrastructure = sink.getTopicCommInfrastructure();
102 this.sourceTopicCommInfrastructure = source.getTopicCommInfrastructure();
105 public boolean send(String message) {
106 return sink.send(message);
109 public void register(TopicListener topicListener) {
110 source.register(topicListener);
113 public boolean offer(String event) {
114 return source.offer(event);
117 public void unregister(TopicListener topicListener) {
118 source.unregister(topicListener);
122 * Determines whether the topic is ready (i.e., {@link #awaitReady(Object, long)} has
123 * previously returned {@code true}).
125 * @return {@code true}, if the topic is ready to send and receive
127 public boolean isReady() {
128 return Boolean.TRUE.equals(checkerQueue.peek());
132 * Waits for the bidirectional topic to become "ready" by publishing a message on the
133 * sink topic and awaiting receipt of the message on the source topic. If the message
134 * is not received within a few seconds, then it tries again. This process is
135 * continued until the message is received, {@link #stopWaiting()} is called, or this thread
136 * is interrupted. Once this returns, subsequent calls will return immediately, always
137 * with the same value.
139 * @param message message to be sent to the sink topic. Note: the equals() method must
140 * return {@code true} if and only if two messages are the same
141 * @param waitMs time to wait, in milliseconds, before re-sending the message
142 * @return {@code true} if the message was received from the source topic,
143 * {@code false} if this method was stopped or interrupted before receipt of
145 * @throws CoderException if the message cannot be encoded
147 public synchronized <T> boolean awaitReady(T message, long waitMs) throws CoderException {
148 // see if we already know the answer
149 if (!checkerQueue.isEmpty()) {
150 return checkerQueue.peek();
153 final String messageText = coder.encode(message);
155 // class of message to be decoded
156 final TopicListener listener = getTopicListener(message);
158 source.register(listener);
160 // loop until the message is received
165 } while ((result = checkerQueue.poll(waitMs, TimeUnit.MILLISECONDS)) == null);
167 // put it back on the queue
168 checkerQueue.add(result);
170 } catch (InterruptedException e) {
171 logger.error("interrupted waiting for topic sink {} source {}", sink.getTopic(), source.getTopic(), e);
172 Thread.currentThread().interrupt();
173 checkerQueue.add(Boolean.FALSE);
176 source.unregister(listener);
179 return checkerQueue.peek();
183 private <T> TopicListener getTopicListener(T message) {
184 @SuppressWarnings("unchecked")
185 final Class<? extends T> clazz = (Class<? extends T>) message.getClass();
187 // create a listener to detect when a matching message is received
188 return (infra, topic, msg) -> {
190 T incoming = decode(msg, clazz);
192 if (message.equals(incoming)) {
193 logger.info("topic {} is ready; found matching message {}", topic, incoming);
194 checkerQueue.add(Boolean.TRUE);
197 } catch (CoderException e) {
198 logger.warn("cannot decode message from topic {}", topic, e);
205 * Stops any listeners that are currently stuck in {@link #awaitReady(Object)} by
206 * adding {@code false} to the queue.
208 public void stopWaiting() {
209 checkerQueue.add(Boolean.FALSE);
212 // these may be overridden by junit tests
214 protected TopicEndpoint getTopicEndpointManager() {
215 return TopicEndpointManager.getManager();
218 protected <T> T decode(String msg, Class<? extends T> clazz) throws CoderException {
219 return coder.decode(msg, clazz);
222 protected void decodeFailed() {
223 // already logged - nothing else to do