4f601fa8b303bb9c7a3b1068974ef8f8e2ab51aa
[policy/common.git] /
1 /*-
2  * ============LICENSE_START=======================================================
3  * ONAP
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
11  *
12  *      http://www.apache.org/licenses/LICENSE-2.0
13  *
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=========================================================
20  */
21
22 package org.onap.policy.common.endpoints.event.comm.client;
23
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;
30 import lombok.Getter;
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;
42
43 /**
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.
46  */
47 @Getter
48 public class BidirectionalTopicClient {
49     private static final Logger logger = LoggerFactory.getLogger(BidirectionalTopicClient.class);
50     private static final Coder coder = new StandardCoder();
51
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;
58
59     /**
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.
65      */
66     private final BlockingDeque<Boolean> checkerQueue = new LinkedBlockingDeque<>();
67
68
69     /**
70      * Constructs the object.
71      *
72      * @param sinkTopic sink topic name
73      * @param sourceTopic source topic name
74      * @throws BidirectionalTopicClientException if either topic does not exist
75      */
76     public BidirectionalTopicClient(String sinkTopic, String sourceTopic) throws BidirectionalTopicClientException {
77         this.sinkTopic = sinkTopic.toLowerCase();
78         this.sourceTopic = sourceTopic.toLowerCase();
79
80         // init sinkClient
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);
86         }
87
88         this.sink = sinks.get(0);
89
90         // init source
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);
96         }
97
98         this.source = sources.get(0);
99
100         this.sinkTopicCommInfrastructure = sink.getTopicCommInfrastructure();
101         this.sourceTopicCommInfrastructure = source.getTopicCommInfrastructure();
102     }
103
104     public boolean send(String message) {
105         return sink.send(message);
106     }
107
108     public void register(TopicListener topicListener) {
109         source.register(topicListener);
110     }
111
112     public boolean offer(String event) {
113         return source.offer(event);
114     }
115
116     public void unregister(TopicListener topicListener) {
117         source.unregister(topicListener);
118     }
119
120     /**
121      * Determines whether the topic is ready (i.e., {@link #awaitReady(Object, long)} has
122      * previously returned {@code true}).
123      *
124      * @return {@code true}, if the topic is ready to send and receive
125      */
126     public boolean isReady() {
127         return Boolean.TRUE.equals(checkerQueue.peek());
128     }
129
130     /**
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.
137      *
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
143      *         the message
144      * @throws CoderException if the message cannot be encoded
145      */
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();
150         }
151
152         final String messageText = coder.encode(message);
153
154         // class of message to be decoded
155         final TopicListener listener = getTopicListener(message);
156
157         source.register(listener);
158
159         // loop until the message is received
160         try {
161             Boolean result;
162             do {
163                 send(messageText);
164             } while ((result = checkerQueue.poll(waitMs, TimeUnit.MILLISECONDS)) == null);
165
166             // put it back on the queue
167             checkerQueue.add(result);
168
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);
173
174         } finally {
175             source.unregister(listener);
176         }
177
178         return checkerQueue.peek();
179     }
180
181     @NotNull
182     private <T> TopicListener getTopicListener(T message) {
183         @SuppressWarnings("unchecked")
184         final Class<? extends T> clazz = (Class<? extends T>) message.getClass();
185
186         // create a listener to detect when a matching message is received
187         return (infra, topic, msg) -> {
188             try {
189                 T incoming = decode(msg, clazz);
190
191                 if (message.equals(incoming)) {
192                     logger.info("topic {} is ready; found matching message {}", topic, incoming);
193                     checkerQueue.add(Boolean.TRUE);
194                 }
195
196             } catch (CoderException e) {
197                 logger.warn("cannot decode message from topic {}", topic, e);
198                 decodeFailed();
199             }
200         };
201     }
202
203     /**
204      * Stops any listeners that are currently stuck in {@link #awaitReady(Object)} by
205      * adding {@code false} to the queue.
206      */
207     public void stopWaiting() {
208         checkerQueue.add(Boolean.FALSE);
209     }
210
211     // these may be overridden by junit tests
212
213     protected TopicEndpoint getTopicEndpointManager() {
214         return TopicEndpointManager.getManager();
215     }
216
217     protected <T> T decode(String msg, Class<? extends T> clazz) throws CoderException {
218         return coder.decode(msg, clazz);
219     }
220
221     protected void decodeFailed() {
222         // already logged - nothing else to do
223     }
224 }