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 jakarta.validation.constraints.NotNull;
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.onap.policy.common.endpoints.event.comm.Topic.CommInfrastructure;
32 import org.onap.policy.common.endpoints.event.comm.TopicEndpoint;
33 import org.onap.policy.common.endpoints.event.comm.TopicEndpointManager;
34 import org.onap.policy.common.endpoints.event.comm.TopicListener;
35 import org.onap.policy.common.endpoints.event.comm.TopicSink;
36 import org.onap.policy.common.endpoints.event.comm.TopicSource;
37 import org.onap.policy.common.utils.coder.Coder;
38 import org.onap.policy.common.utils.coder.CoderException;
39 import org.onap.policy.common.utils.coder.StandardCoder;
40 import org.slf4j.Logger;
41 import org.slf4j.LoggerFactory;
44 * A "bidirectional" topic, which is a pair of topics, one of which is used to publish
45 * requests and the other to receive responses.
48 public class BidirectionalTopicClient {
49 private static final Logger logger = LoggerFactory.getLogger(BidirectionalTopicClient.class);
50 private static final Coder coder = new StandardCoder();
52 private final String sinkTopic;
53 private final String sourceTopic;
54 private final TopicSink sink;
55 private final TopicSource source;
56 private final CommInfrastructure sinkTopicCommInfrastructure;
57 private final CommInfrastructure sourceTopicCommInfrastructure;
60 * Used when checking whether a message sent on the sink topic can be received
61 * on the source topic. When a matching message is received on the incoming topic,
62 * {@code true} is placed on the queue. If {@link #stopWaiting()} is called or the waiting
63 * thread is interrupted, then {@code false} is placed on the queue. Whenever a value
64 * is pulled from the queue, it is immediately placed back on the queue.
66 private final BlockingDeque<Boolean> checkerQueue = new LinkedBlockingDeque<>();
70 * Constructs the object.
72 * @param sinkTopic sink topic name
73 * @param sourceTopic source topic name
74 * @throws BidirectionalTopicClientException if either topic does not exist
76 public BidirectionalTopicClient(String sinkTopic, String sourceTopic) throws BidirectionalTopicClientException {
77 this.sinkTopic = sinkTopic.toLowerCase();
78 this.sourceTopic = sourceTopic.toLowerCase();
81 List<TopicSink> sinks = getTopicEndpointManager().getTopicSinks(sinkTopic);
82 if (sinks.isEmpty()) {
83 throw new BidirectionalTopicClientException("no sinks for topic: " + sinkTopic);
84 } else if (sinks.size() > 1) {
85 throw new BidirectionalTopicClientException("too many sinks for topic: " + sinkTopic);
88 this.sink = sinks.get(0);
91 List<TopicSource> sources = getTopicEndpointManager().getTopicSources(Collections.singletonList(sourceTopic));
92 if (sources.isEmpty()) {
93 throw new BidirectionalTopicClientException("no sources for topic: " + sourceTopic);
94 } else if (sources.size() > 1) {
95 throw new BidirectionalTopicClientException("too many sources for topic: " + sourceTopic);
98 this.source = sources.get(0);
100 this.sinkTopicCommInfrastructure = sink.getTopicCommInfrastructure();
101 this.sourceTopicCommInfrastructure = source.getTopicCommInfrastructure();
104 public boolean send(String message) {
105 return sink.send(message);
108 public void register(TopicListener topicListener) {
109 source.register(topicListener);
112 public boolean offer(String event) {
113 return source.offer(event);
116 public void unregister(TopicListener topicListener) {
117 source.unregister(topicListener);
121 * Determines whether the topic is ready (i.e., {@link #awaitReady(Object, long)} has
122 * previously returned {@code true}).
124 * @return {@code true}, if the topic is ready to send and receive
126 public boolean isReady() {
127 return Boolean.TRUE.equals(checkerQueue.peek());
131 * Waits for the bidirectional topic to become "ready" by publishing a message on the
132 * sink topic and awaiting receipt of the message on the source topic. If the message
133 * is not received within a few seconds, then it tries again. This process is
134 * continued until the message is received, {@link #stopWaiting()} is called, or this thread
135 * is interrupted. Once this returns, subsequent calls will return immediately, always
136 * with the same value.
138 * @param message message to be sent to the sink topic. Note: the equals() method must
139 * return {@code true} if and only if two messages are the same
140 * @param waitMs time to wait, in milliseconds, before re-sending the message
141 * @return {@code true} if the message was received from the source topic,
142 * {@code false} if this method was stopped or interrupted before receipt of
144 * @throws CoderException if the message cannot be encoded
146 public synchronized <T> boolean awaitReady(T message, long waitMs) throws CoderException {
147 // see if we already know the answer
148 if (!checkerQueue.isEmpty()) {
149 return checkerQueue.peek();
152 final String messageText = coder.encode(message);
154 // class of message to be decoded
155 final TopicListener listener = getTopicListener(message);
157 source.register(listener);
159 // loop until the message is received
164 } while ((result = checkerQueue.poll(waitMs, TimeUnit.MILLISECONDS)) == null);
166 // put it back on the queue
167 checkerQueue.add(result);
169 } catch (InterruptedException e) {
170 logger.error("interrupted waiting for topic sink {} source {}", sink.getTopic(), source.getTopic(), e);
171 Thread.currentThread().interrupt();
172 checkerQueue.add(Boolean.FALSE);
175 source.unregister(listener);
178 return checkerQueue.peek();
182 private <T> TopicListener getTopicListener(T message) {
183 @SuppressWarnings("unchecked")
184 final Class<? extends T> clazz = (Class<? extends T>) message.getClass();
186 // create a listener to detect when a matching message is received
187 return (infra, topic, msg) -> {
189 T incoming = decode(msg, clazz);
191 if (message.equals(incoming)) {
192 logger.info("topic {} is ready; found matching message {}", topic, incoming);
193 checkerQueue.add(Boolean.TRUE);
196 } catch (CoderException e) {
197 logger.warn("cannot decode message from topic {}", topic, e);
204 * Stops any listeners that are currently stuck in {@link #awaitReady(Object)} by
205 * adding {@code false} to the queue.
207 public void stopWaiting() {
208 checkerQueue.add(Boolean.FALSE);
211 // these may be overridden by junit tests
213 protected TopicEndpoint getTopicEndpointManager() {
214 return TopicEndpointManager.getManager();
217 protected <T> T decode(String msg, Class<? extends T> clazz) throws CoderException {
218 return coder.decode(msg, clazz);
221 protected void decodeFailed() {
222 // already logged - nothing else to do