1 /*
2 * Copyright 2022-2026 the Pacemaker project contributors
3 *
4 * The version control history for this file may have further details.
5 *
6 * This source code is licensed under the GNU General Public License version 2
7 * or later (GPLv2+) WITHOUT ANY WARRANTY.
8 */
9
10 #include <crm_internal.h>
11
12 #include <stdbool.h>
13
14 #include <crm/common/xml.h>
15
16 #include "pacemaker-attrd.h"
17
18 /* A hash table storing clients that are waiting on a sync point to be reached.
19 * The key is waitlist_client - just a plain int. The obvious key would be
20 * the IPC client's ID, but this is not guaranteed to be unique. A single client
21 * could be waiting on a sync point for multiple attributes at the same time.
22 *
23 * It is not expected that this hash table will ever be especially large.
24 */
25 static GHashTable *waitlist = NULL;
26 static int waitlist_client = 0;
27
28 struct waitlist_node {
29 /* What kind of sync point does this node describe? */
30 enum attrd_sync_point sync_point;
31
32 /* Information required to construct and send a reply to the client. */
33 char *client_id;
34 uint32_t ipc_id;
35 uint32_t flags;
36 };
37
38 /* A hash table storing information on in-progress IPC requests that are awaiting
39 * confirmations. These requests are currently being processed by peer attrds and
40 * we are waiting to receive confirmation messages from each peer indicating that
41 * processing is complete.
42 *
43 * Multiple requests could be waiting on confirmations at the same time.
44 *
45 * The key is the unique callid for the IPC request, and the value is a
46 * confirmation_action struct.
47 */
48 static GHashTable *expected_confirmations = NULL;
49
50 /*!
51 * \internal
52 * \brief A structure describing a single IPC request that is awaiting confirmations
53 */
54 struct confirmation_action {
55 /*!
56 * \brief A list of peer attrds that we are waiting to receive confirmation
57 * messages from
58 *
59 * This list is dynamic - as confirmations arrive from peer attrds, they will
60 * be removed from this list. When the list is empty, all peers have processed
61 * the request and the associated confirmation action will be taken.
62 */
63 GList *respondents;
64
65 /*!
66 * \brief A timer that will be used to remove the client should it time out
67 * before receiving all confirmations
68 */
69 mainloop_timer_t *timer;
70
71 /*!
72 * \brief A function to run when all confirmations have been received
73 */
74 attrd_confirmation_action_fn fn;
75
76 /*!
77 * \brief Information required to construct and send a reply to the client
78 */
79 char *client_id;
80 uint32_t ipc_id;
81 uint32_t flags;
82
83 /*!
84 * \brief The XML request containing the callid associated with this action
85 */
86 void *xml;
87 };
88
89 static void
90 next_key(void)
91 {
92 do {
93 waitlist_client++;
94 if (waitlist_client < 0) {
95 waitlist_client = 1;
96 }
97 } while (g_hash_table_contains(waitlist, GINT_TO_POINTER(waitlist_client)));
98 }
99
100 static void
101 free_waitlist_node(gpointer data)
102 {
103 struct waitlist_node *wl = (struct waitlist_node *) data;
104
105 free(wl->client_id);
106 free(wl);
107 }
108
109 static const char *
110 sync_point_str(enum attrd_sync_point sync_point)
111 {
112 if (sync_point == attrd_sync_point_local) {
113 return PCMK__VALUE_LOCAL;
114 } else if (sync_point == attrd_sync_point_cluster) {
115 return PCMK__VALUE_CLUSTER;
116 } else {
117 return PCMK_VALUE_UNKNOWN;
118 }
119 }
120
121 /*!
122 * \internal
123 * \brief Add a client to the attrd waitlist
124 *
125 * Typically, a client receives an ACK for its XML IPC request immediately. However,
126 * some clients want to wait until their request has been processed and taken effect.
127 * This is called a sync point. Any client placed on this waitlist will have its
128 * ACK message delayed until either its requested sync point is hit, or until it
129 * times out.
130 *
131 * The XML IPC request must specify the type of sync point it wants to wait for.
132 *
133 * \param[in,out] request The request describing the client to place on the waitlist.
134 */
135 void
136 attrd_add_client_to_waitlist(pcmk__request_t *request)
137 {
138 const char *sync_point = attrd_request_sync_point(request->xml);
139 struct waitlist_node *wl = NULL;
140
141 if (sync_point == NULL) {
142 return;
143 }
144
145 if (waitlist == NULL) {
146 waitlist = pcmk__intkey_table(free_waitlist_node);
147 }
148
149 wl = pcmk__assert_alloc(1, sizeof(struct waitlist_node));
150
151 if (pcmk__str_eq(sync_point, PCMK__VALUE_LOCAL, pcmk__str_none)) {
152 wl->sync_point = attrd_sync_point_local;
153 } else if (pcmk__str_eq(sync_point, PCMK__VALUE_CLUSTER, pcmk__str_none)) {
154 wl->sync_point = attrd_sync_point_cluster;
155 } else {
156 free_waitlist_node(wl);
157 return;
158 }
159
160 wl->client_id = pcmk__str_copy(request->ipc_client->id);
161 wl->ipc_id = request->ipc_id;
162 wl->flags = request->flags;
163
164 next_key();
165 pcmk__intkey_table_insert(waitlist, waitlist_client, wl);
166
167 pcmk__trace("Added client %s to waitlist for %s sync point",
168 wl->client_id, sync_point_str(wl->sync_point));
169 pcmk__trace("%u clients now on waitlist", g_hash_table_size(waitlist));
170
171 /* And then add the key to the request XML so we can uniquely identify
172 * it when it comes time to issue the ACK.
173 */
174 pcmk__xe_set_int(request->xml, PCMK__XA_CALL_ID, waitlist_client);
175 }
176
177 /*!
178 * \internal
179 * \brief Free all memory associated with the waitlist. This is most typically
180 * used when attrd shuts down.
181 */
182 void
183 attrd_free_waitlist(void)
184 {
185 g_clear_pointer(&waitlist, g_hash_table_destroy);
186 }
187
188 /*!
189 * \internal
190 * \brief Unconditionally remove a client from the waitlist, such as when the client
191 * node disconnects from the cluster
192 *
193 * \param[in] client The client to remove
194 */
195 void
196 attrd_remove_client_from_waitlist(pcmk__client_t *client)
197 {
198 GHashTableIter iter;
199 gpointer value;
200
201 if (waitlist == NULL) {
202 return;
203 }
204
205 g_hash_table_iter_init(&iter, waitlist);
206
207 while (g_hash_table_iter_next(&iter, NULL, &value)) {
208 struct waitlist_node *wl = (struct waitlist_node *) value;
209
210 if (pcmk__str_eq(wl->client_id, client->id, pcmk__str_none)) {
211 g_hash_table_iter_remove(&iter);
212 pcmk__trace("%u clients now on waitlist",
213 g_hash_table_size(waitlist));
214 }
215 }
216 }
217
218 /*!
219 * \internal
220 * \brief Send an IPC ACK message to all awaiting clients
221 *
222 * This function will search the waitlist for all clients that are currently awaiting
223 * an ACK indicating their attrd operation is complete. Only those clients with a
224 * matching sync point type and callid from their original XML IPC request will be
225 * ACKed. Once they have received an ACK, they will be removed from the waitlist.
226 *
227 * \param[in] sync_point What kind of sync point have we hit?
228 * \param[in] xml The original XML IPC request.
229 */
230 void
231 attrd_ack_waitlist_clients(enum attrd_sync_point sync_point, const xmlNode *xml)
232 {
233 int callid;
234 gpointer value;
235
236 if (waitlist == NULL) {
237 return;
238 }
239
240 if (pcmk__xe_get_int(xml, PCMK__XA_CALL_ID, &callid) != pcmk_rc_ok) {
241 pcmk__warn("Could not get callid from request XML");
242 return;
243 }
244
245 value = pcmk__intkey_table_lookup(waitlist, callid);
246 if (value != NULL) {
247 struct waitlist_node *wl = (struct waitlist_node *) value;
248 pcmk__client_t *client = NULL;
249
250 if (wl->sync_point != sync_point) {
251 return;
252 }
253
254 pcmk__notice("Alerting client %s for reached %s sync point",
255 wl->client_id, sync_point_str(wl->sync_point));
256
257 client = pcmk__find_client_by_id(wl->client_id);
258 if (client == NULL) {
259 return;
260 }
261
262 attrd_send_ack(client, wl->ipc_id, wl->flags | crm_ipc_client_response);
263
264 /* And then remove the client so it doesn't get alerted again. */
265 pcmk__intkey_table_remove(waitlist, callid);
266
267 pcmk__trace("%u clients now on waitlist", g_hash_table_size(waitlist));
268 }
269 }
270
271 /*!
272 * \internal
273 * \brief Action to take when a cluster sync point is hit for a
274 * PCMK__ATTRD_CMD_UPDATE* message.
275 *
276 * \param[in] xml The request that should be passed along to
277 * attrd_ack_waitlist_clients. This should be the original
278 * IPC request containing the callid for this update message.
279 */
280 int
281 attrd_cluster_sync_point_update(xmlNode *xml)
282 {
283 pcmk__trace("Hit cluster sync point for attribute update");
284 attrd_ack_waitlist_clients(attrd_sync_point_cluster, xml);
285 return pcmk_rc_ok;
286 }
287
288 /*!
289 * \internal
290 * \brief Return the sync point attribute for an IPC request
291 *
292 * This function will check both the top-level element of \p xml for a sync
293 * point attribute, as well as all of its \p op children, if any. The latter
294 * is useful for newer versions of attrd that can put multiple IPC requests
295 * into a single message.
296 *
297 * \param[in] xml An XML IPC request
298 *
299 * \note It is assumed that if one child element has a sync point attribute,
300 * all will have a sync point attribute and they will all be the same
301 * sync point. No other configuration is supported.
302 *
303 * \return The sync point attribute of \p xml, or NULL if none.
304 */
305 const char *
306 attrd_request_sync_point(xmlNode *xml)
307 {
308 CRM_CHECK(xml != NULL, return NULL);
309
310 if (xml->children != NULL) {
311 xmlNode *child = pcmk__xe_first_child(xml, PCMK_XE_OP,
312 PCMK__XA_ATTR_SYNC_POINT, NULL);
313
314 if (child) {
315 return pcmk__xe_get(child, PCMK__XA_ATTR_SYNC_POINT);
316 } else {
317 return NULL;
318 }
319
320 } else {
321 return pcmk__xe_get(xml, PCMK__XA_ATTR_SYNC_POINT);
322 }
323 }
324
325 /*!
326 * \internal
327 * \brief Does an IPC request contain any sync point attribute?
328 *
329 * \param[in] xml An XML IPC request
330 *
331 * \return true if there's a sync point attribute, false otherwise
332 */
333 bool
334 attrd_request_has_sync_point(xmlNode *xml)
335 {
336 return attrd_request_sync_point(xml) != NULL;
337 }
338
339 static void
340 free_action(gpointer data)
341 {
342 struct confirmation_action *action = (struct confirmation_action *) data;
343 g_list_free_full(action->respondents, free);
344 mainloop_timer_del(action->timer);
345 pcmk__xml_free(action->xml);
346 free(action->client_id);
347 free(action);
348 }
349
350 /* Remove an IPC request from the expected_confirmations table if the peer attrds
351 * don't respond before the timeout is hit. We set the timeout to 15s. The exact
352 * number isn't critical - we just want to make sure that the table eventually gets
353 * cleared of things that didn't complete.
354 */
355 static gboolean
356 confirmation_timeout_cb(gpointer data)
357 {
358 struct confirmation_action *action = (struct confirmation_action *) data;
359
360 GHashTableIter iter;
361 gpointer value;
362
363 if (expected_confirmations == NULL) {
364 return G_SOURCE_REMOVE;
365 }
366
367 g_hash_table_iter_init(&iter, expected_confirmations);
368
369 while (g_hash_table_iter_next(&iter, NULL, &value)) {
370 if (value == action) {
371 pcmk__client_t *client = pcmk__find_client_by_id(action->client_id);
372 if (client == NULL) {
373 return G_SOURCE_REMOVE;
374 }
375
376 pcmk__trace("Timed out waiting for confirmations for client %s",
377 client->id);
378 pcmk__ipc_send_ack(client, action->ipc_id,
379 action->flags|crm_ipc_client_response,
380 ATTRD_PROTOCOL_VERSION, CRM_EX_TIMEOUT);
381
382 g_hash_table_iter_remove(&iter);
383 pcmk__trace("%u requests now in expected confirmations table",
384 g_hash_table_size(expected_confirmations));
385 break;
386 }
387 }
388
389 return G_SOURCE_REMOVE;
390 }
391
392 /*!
393 * \internal
394 * \brief When a peer disconnects from the cluster, no longer wait for its confirmation
395 * for any IPC action. If this peer is the last one being waited on, this will
396 * trigger the confirmation action.
397 *
398 * \param[in] host The disconnecting peer attrd's uname
399 */
400 void
401 attrd_do_not_expect_from_peer(const char *host)
402 {
403 GList *keys = NULL;
404
405 if (expected_confirmations == NULL) {
406 return;
407 }
408
409 keys = g_hash_table_get_keys(expected_confirmations);
410
411 pcmk__trace("Removing peer %s from expected confirmations", host);
412
413 for (GList *node = keys; node != NULL; node = node->next) {
414 int callid = *(int *) node->data;
415 attrd_handle_confirmation(callid, host);
416 }
417
418 g_list_free(keys);
419 }
420
421 /*!
422 * \internal
423 * \brief When a client disconnects from the cluster, no longer wait on confirmations
424 * for it. Because the peer attrds may still be processing the original IPC
425 * message, they may still send us confirmations. However, we will take no
426 * action on them.
427 *
428 * \param[in] client The disconnecting client
429 */
430 void
431 attrd_do_not_wait_for_client(pcmk__client_t *client)
432 {
433 GHashTableIter iter;
434 gpointer value;
435
436 if (expected_confirmations == NULL) {
437 return;
438 }
439
440 g_hash_table_iter_init(&iter, expected_confirmations);
441
442 while (g_hash_table_iter_next(&iter, NULL, &value)) {
443 struct confirmation_action *action = (struct confirmation_action *) value;
444
445 if (pcmk__str_eq(action->client_id, client->id, pcmk__str_none)) {
446 pcmk__trace("Removing client %s from expected confirmations",
447 client->id);
448 g_hash_table_iter_remove(&iter);
449 pcmk__trace("%u requests now in expected confirmations table",
450 g_hash_table_size(expected_confirmations));
451 break;
452 }
453 }
454 }
455
456 /*!
457 * \internal
458 * \brief Register some action to be taken when IPC request confirmations are
459 * received
460 *
461 * When this function is called, a list of all peer attrds that support confirming
462 * requests is generated. As confirmations from these peer attrds are received,
463 * they are removed from this list. When the list is empty, the registered action
464 * will be called.
465 *
466 * \note This function should always be called before attrd_send_message is called
467 * to broadcast to the peers to ensure that we know what replies we are
468 * waiting on. Otherwise, it is possible the peer could finish and confirm
469 * before we know to expect it.
470 *
471 * \param[in] request The request that is awaiting confirmations
472 * \param[in] fn A function to be run after all confirmations are received
473 */
474 void
475 attrd_expect_confirmations(pcmk__request_t *request, attrd_confirmation_action_fn fn)
476 {
477 struct confirmation_action *action = NULL;
478 GHashTableIter iter;
479 gpointer host, ver;
480 GList *respondents = NULL;
481 int callid;
482
483 if (expected_confirmations == NULL) {
484 expected_confirmations = pcmk__intkey_table((GDestroyNotify) free_action);
485 }
486
487 if (pcmk__xe_get_int(request->xml, PCMK__XA_CALL_ID,
488 &callid) != pcmk_rc_ok) {
489 pcmk__err("Could not get callid from xml");
490 return;
491 }
492
493 if (pcmk__intkey_table_lookup(expected_confirmations, callid)) {
494 pcmk__err("Already waiting on confirmations for call id %d", callid);
495 return;
496 }
497
498 g_hash_table_iter_init(&iter, peer_protocol_vers);
499 while (g_hash_table_iter_next(&iter, &host, &ver)) {
500 if (ATTRD_SUPPORTS_CONFIRMATION(GPOINTER_TO_INT(ver))) {
501 respondents = g_list_prepend(respondents,
502 pcmk__str_copy((char *) host));
503 }
504 }
505
506 action = pcmk__assert_alloc(1, sizeof(struct confirmation_action));
507
508 action->respondents = respondents;
509 action->fn = fn;
510 action->xml = pcmk__xml_copy(NULL, request->xml);
511 action->client_id = pcmk__str_copy(request->ipc_client->id);
512 action->ipc_id = request->ipc_id;
513 action->flags = request->flags;
514
515 action->timer = mainloop_timer_add(NULL, 15000, FALSE, confirmation_timeout_cb, action);
516 mainloop_timer_start(action->timer);
517
518 pcmk__intkey_table_insert(expected_confirmations, callid, action);
519 pcmk__trace("Callid %d now waiting on %u confirmations", callid,
520 g_list_length(respondents));
521 pcmk__trace("%u requests now in expected confirmations table",
522 g_hash_table_size(expected_confirmations));
523 }
524
525 void
526 attrd_free_confirmations(void)
527 {
|
CID (unavailable; MK=ea53f96c13cab808ee1b8783848fc084) (#1 of 1): Inconsistent C union access (INCONSISTENT_UNION_ACCESS): |
|
(1) Event assign_union_field: |
The union field "in" of "_pp" is written. |
|
(2) Event inconsistent_union_field_access: |
In "_pp.out", the union field used: "out" is inconsistent with the field most recently stored: "in". |
528 g_clear_pointer(&expected_confirmations, g_hash_table_destroy);
529 }
530
531 /*!
532 * \internal
533 * \brief Process a confirmation message from a peer attrd
534 *
535 * This function is called every time a PCMK__ATTRD_CMD_CONFIRM message is
536 * received from a peer attrd. If this is the last confirmation we are waiting
537 * on for a given operation, the registered action will be called.
538 *
539 * \param[in] callid The unique callid for the XML IPC request
540 * \param[in] host The confirming peer attrd's uname
541 */
542 void
543 attrd_handle_confirmation(int callid, const char *host)
544 {
545 struct confirmation_action *action = NULL;
546 GList *node = NULL;
547
548 if (expected_confirmations == NULL) {
549 return;
550 }
551
552 action = pcmk__intkey_table_lookup(expected_confirmations, callid);
553 if (action == NULL) {
554 return;
555 }
556
557 node = g_list_find_custom(action->respondents, host, (GCompareFunc) strcasecmp);
558
559 if (node == NULL) {
560 return;
561 }
562
563 action->respondents = g_list_remove(action->respondents, node->data);
564 pcmk__trace("Callid %d now waiting on %u confirmations", callid,
565 g_list_length(action->respondents));
566
567 if (action->respondents == NULL) {
568 action->fn(action->xml);
569 pcmk__intkey_table_remove(expected_confirmations, callid);
570 pcmk__trace("%u requests now in expected confirmations table",
571 g_hash_table_size(expected_confirmations));
572 }
573 }
574