Line data Source code
1 : /**
2 : * @file
3 : *
4 : * @brief
5 : *
6 : * @copyright BSD License (see LICENSE.md or https://www.libelektra.org)
7 : */
8 :
9 : #include "zeromqsend.h"
10 :
11 : #include <kdbhelper.h>
12 : #include <kdblogger.h>
13 :
14 : #include <time.h> // clock_gettime()
15 : #include <unistd.h> // usleep()
16 :
17 : /** wait inside loop while waiting for messages (10ms) */
18 : #define ELEKTRA_ZEROMQSEND_LOOPDELAY_NS (10 * 1000 * 1000)
19 :
20 : /** first byte of a subscription message */
21 : #define ELEKTRA_ZEROMQSEND_SUBSCRIPTION_MESSAGE '\x01'
22 :
23 : #define ELEKTRA_ZEROMQSEND_MONITOR_ENDPOINT "inproc://zmqpublish-monitor"
24 :
25 : /**
26 : * Receive and return events from a ZeroMQ monitor socket.
27 : *
28 : * @param monitor monitor socket
29 : * @return ZMQ_EVENT_ number
30 : * @retval 0 if receiving interrupted or no message available
31 : * @retval -1 on invalid message
32 : */
33 0 : static int getMonitorEvent (void * monitor)
34 : {
35 : // First frame in message contains event number and value
36 : zmq_msg_t msg;
37 0 : zmq_msg_init (&msg);
38 0 : if (zmq_msg_recv (&msg, monitor, ZMQ_DONTWAIT) == -1)
39 : {
40 : // presumably interrupted or no message available
41 : return 0;
42 : }
43 0 : if (!zmq_msg_more (&msg))
44 : {
45 : ELEKTRA_LOG_WARNING ("Invalid monitor message received!");
46 : return -1;
47 : }
48 :
49 0 : uint8_t * data = (uint8_t *) zmq_msg_data (&msg);
50 0 : uint16_t event = *(uint16_t *) (data);
51 :
52 : // Second frame in message contains event address
53 : // We receive it to clear the buffer, since we are only
54 : // interested in the event number
55 0 : zmq_msg_init (&msg);
56 0 : if (zmq_msg_recv (&msg, monitor, ZMQ_DONTWAIT) == -1)
57 : {
58 : // presumably interrupted
59 : return 0;
60 : }
61 0 : if (zmq_msg_more (&msg))
62 : {
63 : ELEKTRA_LOG_WARNING ("Invalid monitor message received!");
64 : return -1;
65 : }
66 :
67 0 : return event;
68 : }
69 :
70 : static struct timespec ts_diff (struct timespec now, struct timespec start)
71 : {
72 : struct timespec diff;
73 0 : if ((now.tv_nsec - start.tv_nsec) < 0)
74 : {
75 0 : diff.tv_sec = now.tv_sec - start.tv_sec - 1;
76 0 : diff.tv_nsec = 1000000000 + now.tv_nsec - start.tv_nsec;
77 : }
78 : else
79 : {
80 0 : diff.tv_sec = now.tv_sec - start.tv_sec;
81 0 : diff.tv_nsec = now.tv_nsec - start.tv_nsec;
82 : }
83 : return diff;
84 : }
85 :
86 : /**
87 : * Wait for connection message from ZeroMQ monitor socket.
88 : *
89 : * @param monitorSocket socket
90 : * @retval 1 when connected
91 : * @retval -1 on timeout
92 : * @retval 0 on other errors
93 : */
94 0 : static int waitForConnection (void * monitorSocket, long connectTimeout)
95 : {
96 : struct timespec wait;
97 : struct timespec start;
98 : struct timespec now;
99 : struct timespec diff;
100 0 : time_t startFallback = -1;
101 0 : long timeoutSec = (connectTimeout / (1000));
102 0 : long timeoutNsec = (connectTimeout % (1000)) * (1000 * 1000);
103 0 : if (clock_gettime (CLOCK_MONOTONIC, &start) == -1)
104 : {
105 : ELEKTRA_LOG_WARNING ("Using slower fallback for timeout detection");
106 0 : startFallback = time (NULL);
107 : // minimum timeout is 1 second when using the fallback
108 0 : if (timeoutSec == 0)
109 : {
110 0 : timeoutSec = 1;
111 : }
112 : }
113 :
114 : // wait for connection established event
115 0 : int connected = 0;
116 0 : while (!connected)
117 : {
118 0 : wait.tv_sec = 0;
119 0 : wait.tv_nsec = ELEKTRA_ZEROMQSEND_LOOPDELAY_NS;
120 0 : while (!nanosleep (&wait, &wait) && errno == EINTR)
121 : ;
122 :
123 0 : int event = getMonitorEvent (monitorSocket);
124 :
125 0 : int timeout = 0;
126 0 : if (startFallback == -1)
127 : {
128 0 : clock_gettime (CLOCK_MONOTONIC, &now);
129 0 : diff = ts_diff (now, start);
130 0 : timeout = diff.tv_sec >= timeoutSec && diff.tv_nsec >= timeoutNsec;
131 : }
132 : else
133 : {
134 0 : timeout = time (NULL) - startFallback >= timeoutSec;
135 : }
136 0 : if (timeout)
137 : {
138 : ELEKTRA_LOG_WARNING ("connection timed out. could not publish notification");
139 0 : zmq_close (monitorSocket);
140 0 : return -1;
141 : }
142 :
143 0 : switch (event)
144 : {
145 : case ZMQ_EVENT_CONNECTED:
146 : // we do not need the publisher monitor anymore
147 0 : zmq_close (monitorSocket);
148 0 : connected = 1;
149 0 : break;
150 : case -1:
151 : // abort, inconsistencies detected
152 : ELEKTRA_LOG_WARNING ("Cannot monitor connection events");
153 : return 0;
154 : break;
155 : case 0:
156 : // no message available or interrupted, try again
157 : break;
158 : default:
159 : // other ZMQ event, ignore
160 : break;
161 : }
162 : }
163 : return 1;
164 : }
165 :
166 : /**
167 : * Wait for first subscription message on ZeroMQ socket.
168 : *
169 : * @param socket socket
170 : * @retval 1 on success
171 : * @retval -2 on timeout
172 : * @retval 0 on other errors
173 : */
174 0 : static int waitForSubscription (void * socket, long subscribeTimeout)
175 : {
176 : struct timespec start;
177 : struct timespec now;
178 : struct timespec wait;
179 : struct timespec diff;
180 0 : time_t startFallback = -1;
181 0 : long timeoutSec = (subscribeTimeout / (1000));
182 0 : long timeoutNsec = (subscribeTimeout % (1000)) * (1000 * 1000);
183 0 : if (clock_gettime (CLOCK_MONOTONIC, &start) == -1)
184 : {
185 : ELEKTRA_LOG_WARNING ("Using slower fallback for timeout detection");
186 0 : startFallback = time (NULL);
187 : // minimum timeout is 1 second when using the fallback
188 0 : if (timeoutSec == 0)
189 : {
190 0 : timeoutSec = 1;
191 : }
192 : }
193 :
194 : // wait until we receive the first subscription message
195 : zmq_msg_t message;
196 0 : zmq_msg_init (&message);
197 0 : int hasSubscriber = 0;
198 0 : int lastErrno = 0;
199 : do
200 : {
201 0 : wait.tv_sec = 0;
202 0 : wait.tv_nsec = ELEKTRA_ZEROMQSEND_LOOPDELAY_NS;
203 0 : while (!nanosleep (&wait, &wait) && errno == EINTR)
204 : ;
205 :
206 0 : lastErrno = 0;
207 0 : int result = zmq_msg_recv (&message, socket, ZMQ_DONTWAIT);
208 0 : if (result == -1)
209 : {
210 0 : lastErrno = zmq_errno ();
211 : }
212 :
213 0 : int timeout = 0;
214 0 : if (startFallback == -1)
215 : {
216 0 : clock_gettime (CLOCK_MONOTONIC, &now);
217 0 : diff = ts_diff (now, start);
218 0 : timeout = diff.tv_sec >= timeoutSec && diff.tv_nsec >= timeoutNsec;
219 : }
220 : else
221 : {
222 0 : timeout = time (NULL) - startFallback >= timeoutSec;
223 : }
224 0 : if (timeout)
225 : {
226 : ELEKTRA_LOG_WARNING ("subscribing timed out. could not publish notification");
227 0 : zmq_msg_close (&message);
228 0 : return -2;
229 : }
230 :
231 0 : if (result == -1)
232 : {
233 0 : if (lastErrno != EAGAIN)
234 : {
235 : ELEKTRA_LOG_WARNING ("receiving failed %s", zmq_strerror (lastErrno));
236 0 : zmq_msg_close (&message);
237 0 : return 0;
238 : }
239 : }
240 : else
241 : {
242 : // we have received a message subscription or unsubscription message
243 0 : char * messageData = zmq_msg_data (&message);
244 0 : if (messageData[0] == ELEKTRA_ZEROMQSEND_SUBSCRIPTION_MESSAGE)
245 : {
246 0 : hasSubscriber = 1;
247 : }
248 : }
249 0 : } while (lastErrno == EAGAIN && !hasSubscriber);
250 :
251 0 : zmq_msg_close (&message);
252 :
253 0 : return 1;
254 : }
255 :
256 : /**
257 : * @internal
258 : * Connect to ZeroMq SUB or XSUB socket bound at endpoint.
259 : *
260 : * @param data plugin data
261 : * @retval 1 on success
262 : * @retval 0 on error
263 : */
264 0 : int elektraZeroMqSendConnect (ElektraZeroMqSendPluginData * data)
265 : {
266 : // create zmq context
267 0 : if (!data->zmqContext)
268 : {
269 0 : data->zmqContext = zmq_ctx_new ();
270 0 : if (data->zmqContext == NULL)
271 : {
272 : ELEKTRA_LOG_WARNING ("zmq_ctx_new failed %s", zmq_strerror (zmq_errno ()));
273 : return 0;
274 : }
275 : }
276 :
277 0 : if (!data->zmqPublisher)
278 : {
279 : // create publish socket
280 0 : data->zmqPublisher = zmq_socket (data->zmqContext, ZMQ_XPUB);
281 0 : if (data->zmqPublisher == NULL)
282 : {
283 : ELEKTRA_LOG_WARNING ("zmq_socket failed %s", zmq_strerror (zmq_errno ()));
284 0 : zmq_close (data->zmqPublisher);
285 0 : return 0;
286 : }
287 :
288 : // setup socket monitor
289 0 : if (zmq_socket_monitor (data->zmqPublisher, ELEKTRA_ZEROMQSEND_MONITOR_ENDPOINT, ZMQ_EVENT_CONNECTED) == -1)
290 : {
291 : ELEKTRA_LOG_WARNING ("creating socket monitor failed: %s", zmq_strerror (zmq_errno ()));
292 : return 0;
293 : }
294 0 : data->zmqPublisherMonitor = zmq_socket (data->zmqContext, ZMQ_PAIR);
295 0 : if (zmq_connect (data->zmqPublisherMonitor, ELEKTRA_ZEROMQSEND_MONITOR_ENDPOINT) != 0)
296 : {
297 : ELEKTRA_LOG_WARNING ("connecting to socket monitor failed: %s", zmq_strerror (zmq_errno ()));
298 : return 0;
299 : }
300 :
301 : // connect to endpoint
302 0 : int result = zmq_connect (data->zmqPublisher, data->endpoint);
303 0 : if (result != 0)
304 : {
305 : ELEKTRA_LOG_WARNING ("zmq_connect error: %s", zmq_strerror (zmq_errno ()));
306 0 : zmq_close (data->zmqPublisher);
307 0 : data->zmqPublisher = NULL;
308 0 : return 0;
309 : }
310 : }
311 :
312 : return 1;
313 : }
314 :
315 : /**
316 : * Publish notification on ZeroMq connection.
317 : *
318 : * @param changeType type of change
319 : * @param keyName name of changed key
320 : * @param data plugin data
321 : * @retval 1 on success
322 : * @retval -1 on connection timeout
323 : * @retval -2 on subscription timeout
324 : * @retval 0 on other errors
325 : */
326 0 : int elektraZeroMqSendPublish (const char * changeType, const char * keyName, ElektraZeroMqSendPluginData * data)
327 : {
328 0 : if (!elektraZeroMqSendConnect (data))
329 : {
330 : ELEKTRA_LOG_WARNING ("could not connect to endpoint");
331 : return 0;
332 : }
333 :
334 : // wait for subscription message
335 0 : if (!data->hasSubscriber)
336 : {
337 : // NOTE zmq_connect() returns before a connection is established since
338 : // ZeroMq asynchronously does that in the background.
339 : // All notifications sent before the connection is established and and the
340 : // socket has a subscriber are lost since ZMQ_(X)PUB sockets handle message
341 : // filtering: Without subscribers all messages are discarded.
342 : // Therefore we monitor the socket for until the connection is established
343 : // and then wait for the first subscription message.
344 : // A ZMQ_XPUB socket instead of a ZMQ_PUB socket allows us to receive
345 : // subscription messages
346 0 : int result = waitForConnection (data->zmqPublisherMonitor, data->connectTimeout);
347 0 : if (result != 1)
348 : {
349 : return result;
350 : }
351 0 : result = waitForSubscription (data->zmqPublisher, data->subscribeTimeout);
352 0 : if (result == 1)
353 : {
354 0 : data->hasSubscriber = 1;
355 : }
356 : else
357 : {
358 : return result;
359 : }
360 : }
361 :
362 : // send notification
363 0 : if (!elektraZeroMqSendNotification (data->zmqPublisher, changeType, keyName))
364 : {
365 : ELEKTRA_LOG_WARNING ("could not send notification");
366 : return 0;
367 : }
368 :
369 0 : return 1;
370 : }
371 :
372 : /**
373 : * @internal
374 : * Send notification over ZeroMq socket.
375 : *
376 : * zmq_send() asynchronous.
377 : * Processing already handled in a thread created by ZeroMq.
378 : *
379 : * @param socket ZeroMq socket
380 : * @param changeType type of change
381 : * @param keyName name of changed key
382 : * @retval 1 on success
383 : * @retval 0 on error
384 : */
385 0 : int elektraZeroMqSendNotification (void * socket, const char * changeType, const char * keyName)
386 : {
387 : unsigned int size;
388 :
389 : // Send change type
390 0 : size = zmq_send (socket, changeType, elektraStrLen (changeType), ZMQ_SNDMORE);
391 0 : if (size != elektraStrLen (changeType))
392 : {
393 : return 0;
394 : }
395 :
396 0 : size = zmq_send (socket, keyName, elektraStrLen (keyName), 0);
397 0 : if (size != elektraStrLen (keyName))
398 : {
399 : return 0;
400 : }
401 :
402 0 : return 1;
403 : }
|