| /* |
| * Licensed to the Apache Software Foundation (ASF) under one |
| * or more contributor license agreements. See the NOTICE file |
| * distributed with this work for additional information |
| * regarding copyright ownership. The ASF licenses this file |
| * to you under the Apache License, Version 2.0 (the |
| * "License"); you may not use this file except in compliance |
| * with the License. You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, |
| * software distributed under the License is distributed on an |
| * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY |
| * KIND, either express or implied. See the License for the |
| * specific language governing permissions and limitations |
| * under the License. |
| */ |
| |
| #include <qpid/dispatch/python_embedded.h> |
| #include <stdio.h> |
| #include <string.h> |
| #include <stdbool.h> |
| #include <stdlib.h> |
| #include <qpid/dispatch.h> |
| #include "dispatch_private.h" |
| #include "router_private.h" |
| |
| static char *module = "ROUTER"; |
| |
| static char *router_role = "inter-router"; |
| static char *local_prefix = "_local/"; |
| static char *topo_prefix = "_topo/"; |
| static char *direct_prefix; |
| static char *node_id; |
| |
| static qd_address_semantics_t router_semantics = {true, QD_FORWARD_MULTICAST}; |
| static qd_address_semantics_t default_semantics = {false, QD_FORWARD_MULTICAST}; |
| |
| /** |
| * Address Types and Processing: |
| * |
| * Address Hash Key onReceive |
| * =================================================================== |
| * _local/<local> L<local> handler |
| * _topo/<area>/<router>/<local> A<area> forward |
| * _topo/<my-area>/<router>/<local> R<router> forward |
| * _topo/<my-area>/<my-router>/<local> L<local> handler |
| * _topo/<area>/all/<local> A<area> forward |
| * _topo/<my-area>/all/<local> L<local> forward handler |
| * _topo/all/all/<local> L<local> forward handler |
| * <mobile> M<mobile> forward handler |
| */ |
| |
| ALLOC_DEFINE(qd_routed_event_t); |
| ALLOC_DEFINE(qd_router_link_t); |
| ALLOC_DEFINE(qd_router_node_t); |
| ALLOC_DEFINE(qd_router_ref_t); |
| ALLOC_DEFINE(qd_router_link_ref_t); |
| ALLOC_DEFINE(qd_address_t); |
| ALLOC_DEFINE(qd_router_conn_t); |
| |
| |
| void qd_router_add_link_ref_LH(qd_router_link_ref_list_t *ref_list, qd_router_link_t *link) |
| { |
| qd_router_link_ref_t *ref = new_qd_router_link_ref_t(); |
| DEQ_ITEM_INIT(ref); |
| ref->link = link; |
| link->ref = ref; |
| DEQ_INSERT_TAIL(*ref_list, ref); |
| } |
| |
| |
| void qd_router_del_link_ref_LH(qd_router_link_ref_list_t *ref_list, qd_router_link_t *link) |
| { |
| if (link->ref) { |
| DEQ_REMOVE(*ref_list, link->ref); |
| free_qd_router_link_ref_t(link->ref); |
| link->ref = 0; |
| } |
| } |
| |
| |
| void qd_router_add_node_ref_LH(qd_router_ref_list_t *ref_list, qd_router_node_t *rnode) |
| { |
| qd_router_ref_t *ref = new_qd_router_ref_t(); |
| DEQ_ITEM_INIT(ref); |
| ref->router = rnode; |
| rnode->ref_count++; |
| DEQ_INSERT_TAIL(*ref_list, ref); |
| } |
| |
| |
| void qd_router_del_node_ref_LH(qd_router_ref_list_t *ref_list, qd_router_node_t *rnode) |
| { |
| qd_router_ref_t *ref = DEQ_HEAD(*ref_list); |
| while (ref) { |
| if (ref->router == rnode) { |
| DEQ_REMOVE(*ref_list, ref); |
| free_qd_router_ref_t(ref); |
| rnode->ref_count--; |
| break; |
| } |
| ref = DEQ_NEXT(ref); |
| } |
| } |
| |
| |
| /** |
| * Check an address to see if it no longer has any associated destinations. |
| * Depending on its policy, the address may be eligible for being closed out |
| * (i.e. Logging its terminal statistics and freeing its resources). |
| */ |
| void qd_router_check_addr(qd_router_t *router, qd_address_t *addr, int was_local) |
| { |
| if (addr == 0) |
| return; |
| |
| unsigned char *key = 0; |
| int to_delete = 0; |
| int no_more_locals = 0; |
| |
| sys_mutex_lock(router->lock); |
| |
| // |
| // If the address has no handlers or destinations, it should be deleted. |
| // |
| if (addr->handler == 0 && DEQ_SIZE(addr->rlinks) == 0 && DEQ_SIZE(addr->rnodes) == 0) |
| to_delete = 1; |
| |
| // |
| // If we have just removed a local linkage and it was the last local linkage, |
| // we need to notify the router module that there is no longer a local |
| // presence of this address. |
| // |
| if (was_local && DEQ_SIZE(addr->rlinks) == 0) |
| no_more_locals = 1; |
| |
| if (to_delete) { |
| // |
| // Delete the address but grab the hash key so we can use it outside the |
| // critical section. |
| // |
| qd_hash_remove_by_handle2(router->addr_hash, addr->hash_handle, &key); |
| DEQ_REMOVE(router->addrs, addr); |
| qd_hash_handle_free(addr->hash_handle); |
| free_qd_address_t(addr); |
| } |
| |
| // |
| // If we're not deleting but there are no more locals, get a copy of the hash key. |
| // |
| if (!to_delete && no_more_locals) { |
| const unsigned char *key_const = qd_hash_key_by_handle(addr->hash_handle); |
| key = (unsigned char*) malloc(strlen((const char*) key_const) + 1); |
| strcpy((char*) key, (const char*) key_const); |
| } |
| |
| sys_mutex_unlock(router->lock); |
| |
| // |
| // If the address is mobile-class and it was just removed from a local link, |
| // tell the router module that it is no longer attached locally. |
| // |
| if (no_more_locals && key && key[0] == 'M') |
| qd_router_mobile_removed(router, (const char*) key); |
| |
| // |
| // Free the key that was not freed by the hash table. |
| // |
| if (key) |
| free(key); |
| } |
| |
| |
| /** |
| * Determine whether a connection is configured in the inter-router role. |
| */ |
| static int qd_router_connection_is_inter_router(const qd_connection_t *conn) |
| { |
| if (!conn) |
| return 0; |
| |
| const qd_server_config_t *cf = qd_connection_config(conn); |
| if (cf && strcmp(cf->role, router_role) == 0) |
| return 1; |
| |
| return 0; |
| } |
| |
| |
| /** |
| * Determine whether a terminus has router capability |
| */ |
| static int qd_router_terminus_is_router(pn_terminus_t *term) |
| { |
| pn_data_t *cap = pn_terminus_capabilities(term); |
| |
| pn_data_rewind(cap); |
| pn_data_next(cap); |
| if (cap && pn_data_type(cap) == PN_SYMBOL) { |
| pn_bytes_t sym = pn_data_get_symbol(cap); |
| if (sym.size == strlen(QD_CAPABILITY_ROUTER) && |
| strcmp(sym.start, QD_CAPABILITY_ROUTER) == 0) |
| return 1; |
| } |
| |
| return 0; |
| } |
| |
| |
| static void qd_router_generate_temp_addr(qd_router_t *router, char *buffer, size_t length) |
| { |
| static const char *table = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789+_"; |
| char discriminator[11]; |
| long int rnd = random(); |
| int idx; |
| |
| for (idx = 0; idx < 6; idx++) |
| discriminator[idx] = table[(rnd >> (idx * 6)) & 63]; |
| discriminator[idx] = '\0'; |
| |
| snprintf(buffer, length, "amqp:/%s%s/%s/temp.%s", topo_prefix, router->router_area, router->router_id, discriminator); |
| } |
| |
| |
| static int qd_router_find_mask_bit_LH(qd_router_t *router, qd_link_t *link) |
| { |
| qd_router_conn_t *shared = (qd_router_conn_t*) qd_link_get_conn_context(link); |
| if (shared) |
| return shared->mask_bit; |
| |
| int mask_bit; |
| if (qd_bitmask_first_set(router->neighbor_free_mask, &mask_bit)) { |
| qd_bitmask_clear_bit(router->neighbor_free_mask, mask_bit); |
| } else { |
| qd_log(module, LOG_CRITICAL, "Exceeded maximum inter-router link count"); |
| return -1; |
| } |
| |
| shared = new_qd_router_conn_t(); |
| shared->mask_bit = mask_bit; |
| qd_link_set_conn_context(link, shared); |
| return mask_bit; |
| } |
| |
| |
| /** |
| * Outgoing Link Writable Handler |
| */ |
| static int router_writable_link_handler(void* context, qd_link_t *link) |
| { |
| qd_router_t *router = (qd_router_t*) context; |
| qd_delivery_t *delivery; |
| qd_router_link_t *rlink = (qd_router_link_t*) qd_link_get_context(link); |
| pn_link_t *pn_link = qd_link_pn(link); |
| uint64_t tag; |
| int link_credit = pn_link_credit(pn_link); |
| qd_routed_event_list_t to_send; |
| qd_routed_event_list_t events; |
| qd_routed_event_t *re; |
| size_t offer; |
| int event_count = 0; |
| bool drain_mode; |
| bool drain_changed = qd_link_drain_changed(link, &drain_mode); |
| |
| DEQ_INIT(to_send); |
| DEQ_INIT(events); |
| |
| sys_mutex_lock(router->lock); |
| |
| // |
| // Pull the non-delivery events into a local list so they can be processed without |
| // the lock being held. |
| // |
| re = DEQ_HEAD(rlink->event_fifo); |
| while (re) { |
| DEQ_REMOVE_HEAD(rlink->event_fifo); |
| DEQ_INSERT_TAIL(events, re); |
| re = DEQ_HEAD(rlink->event_fifo); |
| } |
| |
| // |
| // Under lock, move available deliveries from the msg_fifo to the local to_send |
| // list. Don't move more than we have credit to send. |
| // |
| if (link_credit > 0) { |
| tag = router->dtag; |
| re = DEQ_HEAD(rlink->msg_fifo); |
| while (re) { |
| DEQ_REMOVE_HEAD(rlink->msg_fifo); |
| DEQ_INSERT_TAIL(to_send, re); |
| if (DEQ_SIZE(to_send) == link_credit) |
| break; |
| re = DEQ_HEAD(rlink->msg_fifo); |
| } |
| router->dtag += DEQ_SIZE(to_send); |
| } |
| |
| offer = DEQ_SIZE(rlink->msg_fifo); |
| sys_mutex_unlock(router->lock); |
| |
| // |
| // Deliver all the to_send messages downrange |
| // |
| re = DEQ_HEAD(to_send); |
| while (re) { |
| DEQ_REMOVE_HEAD(to_send); |
| |
| // |
| // Get a delivery for the send. This will be the current delivery on the link. |
| // |
| tag++; |
| delivery = qd_delivery(link, pn_dtag((char*) &tag, 8)); |
| |
| // |
| // Send the message |
| // |
| qd_message_send(re->message, link); |
| |
| // |
| // If there is an incoming delivery associated with this message, link it |
| // with the outgoing delivery. Otherwise, the message arrived pre-settled |
| // and should be sent presettled. |
| // |
| sys_mutex_lock(router->lock); |
| if (re->delivery) { |
| if (qd_delivery_fifo_exit_LH(re->delivery)) |
| qd_delivery_link_peers_LH(re->delivery, delivery); |
| } else |
| qd_delivery_free_LH(delivery, 0); // settle and free |
| sys_mutex_unlock(router->lock); |
| |
| pn_link_advance(pn_link); |
| event_count++; |
| |
| qd_message_free(re->message); |
| free_qd_routed_event_t(re); |
| re = DEQ_HEAD(to_send); |
| } |
| |
| // |
| // Process the non-delivery events. |
| // |
| re = DEQ_HEAD(events); |
| while (re) { |
| DEQ_REMOVE_HEAD(events); |
| |
| if (re->delivery) { |
| if (re->disposition) { |
| pn_delivery_update(qd_delivery_pn(re->delivery), re->disposition); |
| event_count++; |
| } |
| |
| sys_mutex_lock(router->lock); |
| |
| bool ok = qd_delivery_fifo_exit_LH(re->delivery); |
| if (ok && re->settle) { |
| qd_delivery_unlink_LH(re->delivery); |
| qd_delivery_free_LH(re->delivery, 0); |
| event_count++; |
| } |
| |
| sys_mutex_unlock(router->lock); |
| } |
| |
| free_qd_routed_event_t(re); |
| re = DEQ_HEAD(events); |
| } |
| |
| // |
| // Set the offer to the number of messages remaining to be sent. |
| // |
| if (offer > 0) |
| pn_link_offered(pn_link, offer); |
| else { |
| pn_link_drained(pn_link); |
| |
| // |
| // If this link is in drain mode and it wasn't last time we came through here, we need to |
| // count this operation as a work event. This will allow the container to process the |
| // connector and send out the flow(drain=true) response to the receiver. |
| // |
| if (drain_changed && drain_mode) |
| event_count++; |
| } |
| |
| return event_count; |
| } |
| |
| |
| static qd_field_iterator_t *router_annotate_message(qd_router_t *router, qd_message_t *msg, int *drop) |
| { |
| qd_parsed_field_t *in_da = qd_message_delivery_annotations(msg); |
| qd_composed_field_t *out_da = qd_compose(QD_PERFORMATIVE_DELIVERY_ANNOTATIONS, 0); |
| qd_field_iterator_t *ingress_iter = 0; |
| |
| qd_parsed_field_t *trace = 0; |
| qd_parsed_field_t *ingress = 0; |
| |
| if (in_da) { |
| trace = qd_parse_value_by_key(in_da, QD_DA_TRACE); |
| ingress = qd_parse_value_by_key(in_da, QD_DA_INGRESS); |
| } |
| |
| qd_compose_start_map(out_da); |
| |
| // |
| // If there is a trace field, append this router's ID to the trace. |
| // |
| qd_compose_insert_string(out_da, QD_DA_TRACE); |
| qd_compose_start_list(out_da); |
| if (trace) { |
| if (qd_parse_is_list(trace)) { |
| uint32_t idx = 0; |
| qd_parsed_field_t *trace_item = qd_parse_sub_value(trace, idx); |
| while (trace_item) { |
| qd_field_iterator_t *iter = qd_parse_raw(trace_item); |
| if (qd_field_iterator_equal(iter, (unsigned char*) node_id)) |
| *drop = 1; |
| qd_field_iterator_reset(iter); |
| qd_compose_insert_string_iterator(out_da, iter); |
| idx++; |
| trace_item = qd_parse_sub_value(trace, idx); |
| } |
| } |
| } |
| |
| qd_compose_insert_string(out_da, node_id); |
| qd_compose_end_list(out_da); |
| |
| // |
| // If there is no ingress field, annotate the ingress as this router else |
| // keep the original field. |
| // |
| qd_compose_insert_string(out_da, QD_DA_INGRESS); |
| if (ingress && qd_parse_is_scalar(ingress)) { |
| ingress_iter = qd_parse_raw(ingress); |
| qd_compose_insert_string_iterator(out_da, ingress_iter); |
| } else |
| qd_compose_insert_string(out_da, node_id); |
| |
| qd_compose_end_map(out_da); |
| |
| qd_message_set_delivery_annotations(msg, out_da); |
| qd_compose_free(out_da); |
| |
| // |
| // Return the iterator to the ingress field _if_ it was present. |
| // If we added the ingress, return NULL. |
| // |
| return ingress_iter; |
| } |
| |
| |
| /** |
| * Inbound Delivery Handler |
| */ |
| static void router_rx_handler(void* context, qd_link_t *link, qd_delivery_t *delivery) |
| { |
| qd_router_t *router = (qd_router_t*) context; |
| pn_link_t *pn_link = qd_link_pn(link); |
| qd_router_link_t *rlink = (qd_router_link_t*) qd_link_get_context(link); |
| qd_message_t *msg; |
| int valid_message = 0; |
| |
| // |
| // Receive the message into a local representation. If the returned message |
| // pointer is NULL, we have not yet received a complete message. |
| // |
| msg = qd_message_receive(delivery); |
| if (!msg) |
| return; |
| |
| // |
| // Consume the delivery and issue a replacement credit |
| // |
| pn_link_advance(pn_link); |
| pn_link_flow(pn_link, 1); |
| |
| sys_mutex_lock(router->lock); |
| |
| // |
| // Handle the Link-Routing case. If this incoming link is associated with a connected |
| // link, simply deliver the message to the outgoing link. There is no need to validate |
| // the message in this case. |
| // |
| if (rlink->connected_link) { |
| qd_router_link_t *clink = rlink->connected_link; |
| qd_routed_event_t *re = new_qd_routed_event_t(); |
| |
| DEQ_ITEM_INIT(re); |
| re->delivery = 0; |
| re->message = msg; |
| re->settle = false; |
| re->disposition = 0; |
| DEQ_INSERT_TAIL(clink->msg_fifo, re); |
| |
| // |
| // If the incoming delivery is settled (pre-settled), don't link it into the routed |
| // event. If it's not settled, link it into the event for later handling. |
| // |
| if (qd_delivery_settled(delivery)) |
| qd_delivery_free_LH(delivery, 0); |
| else { |
| re->delivery = delivery; |
| qd_delivery_fifo_enter_LH(delivery); |
| } |
| |
| sys_mutex_unlock(router->lock); |
| qd_link_activate(clink->link); |
| return; |
| } |
| |
| // |
| // We are performing Message-Routing, therefore we will need to validate the message |
| // through the Properties section so we can access the TO field. |
| // |
| qd_message_t *in_process_copy = 0; |
| qd_router_message_cb_t handler = 0; |
| void *handler_context = 0; |
| |
| valid_message = qd_message_check(msg, QD_DEPTH_PROPERTIES); |
| |
| if (valid_message) { |
| qd_field_iterator_t *iter = qd_message_field_iterator(msg, QD_FIELD_TO); |
| qd_address_t *addr; |
| int fanout = 0; |
| |
| if (iter) { |
| qd_field_iterator_reset_view(iter, ITER_VIEW_ADDRESS_HASH); |
| |
| // |
| // Note: This function is going to need to be refactored so we can put an |
| // asynchronous address lookup here. In the event there is a translation |
| // of the address (via namespace), it will have to be done here after |
| // obtaining the iterator and before doing the hash lookup. |
| // |
| // Note that this lookup is only done for global/mobile class addresses. |
| // |
| |
| qd_hash_retrieve(router->addr_hash, iter, (void*) &addr); |
| qd_field_iterator_reset_view(iter, ITER_VIEW_NO_HOST); |
| int is_local = qd_field_iterator_prefix(iter, local_prefix); |
| int is_direct = qd_field_iterator_prefix(iter, direct_prefix); |
| qd_field_iterator_free(iter); |
| |
| if (addr) { |
| // |
| // If the incoming link is an endpoint link, count this as an ingress delivery. |
| // |
| if (rlink->link_type == QD_LINK_ENDPOINT) |
| addr->deliveries_ingress++; |
| |
| // |
| // To field is valid and contains a known destination. Handle the various |
| // cases for forwarding. |
| // |
| |
| // |
| // Interpret and update the delivery annotations of the message. As a convenience, |
| // this function returns the iterator to the ingress field (if it exists). |
| // |
| int drop = 0; |
| qd_field_iterator_t *ingress_iter = router_annotate_message(router, msg, &drop); |
| |
| // |
| // Forward to the in-process handler for this message if there is one. The |
| // actual invocation of the handler will occur later after we've released |
| // the lock. |
| // |
| if (!drop && addr->handler) { |
| in_process_copy = qd_message_copy(msg); |
| handler = addr->handler; |
| handler_context = addr->handler_context; |
| addr->deliveries_to_container++; |
| } |
| |
| // |
| // If the address form is local (i.e. is prefixed by _local), don't forward |
| // outside of the router process. |
| // |
| if (!drop && !is_local) { |
| // |
| // Forward to all of the local links receiving this address. |
| // |
| qd_router_link_ref_t *dest_link_ref = DEQ_HEAD(addr->rlinks); |
| while (dest_link_ref) { |
| qd_routed_event_t *re = new_qd_routed_event_t(); |
| DEQ_ITEM_INIT(re); |
| re->delivery = 0; |
| re->message = qd_message_copy(msg); |
| re->settle = 0; |
| re->disposition = 0; |
| DEQ_INSERT_TAIL(dest_link_ref->link->msg_fifo, re); |
| |
| fanout++; |
| if (fanout == 1 && !qd_delivery_settled(delivery)) { |
| re->delivery = delivery; |
| qd_delivery_fifo_enter_LH(delivery); |
| } |
| |
| addr->deliveries_egress++; |
| qd_link_activate(dest_link_ref->link->link); |
| dest_link_ref = DEQ_NEXT(dest_link_ref); |
| } |
| |
| // |
| // If the address form is direct to this router node, don't relay it on |
| // to any other part of the network. |
| // |
| if (!is_direct) { |
| // |
| // Get the mask bit associated with the ingress router for the message. |
| // This will be compared against the "valid_origin" masks for each |
| // candidate destination router. |
| // |
| int origin = -1; |
| if (ingress_iter && !addr->semantics->bypass_valid_origins) { |
| qd_field_iterator_reset_view(ingress_iter, ITER_VIEW_NODE_HASH); |
| qd_address_t *origin_addr; |
| qd_hash_retrieve(router->addr_hash, ingress_iter, (void*) &origin_addr); |
| if (origin_addr && DEQ_SIZE(origin_addr->rnodes) == 1) { |
| qd_router_ref_t *rref = DEQ_HEAD(origin_addr->rnodes); |
| origin = rref->router->mask_bit; |
| } |
| } else |
| origin = 0; |
| |
| // |
| // Forward to the next-hops for remote destinations. |
| // |
| if (origin >= 0) { |
| qd_router_ref_t *dest_node_ref = DEQ_HEAD(addr->rnodes); |
| qd_router_link_t *dest_link; |
| qd_bitmask_t *link_set = qd_bitmask(0); |
| |
| // |
| // Loop over the target nodes for this address. Build a set of outgoing links |
| // for which there are valid targets. We do this to avoid sending more than one |
| // message down a given link. It's possible that there are multiple destinations |
| // for this address that are all reachable over the same link. In this case, we |
| // will send only one copy of the message over the link and allow a downstream |
| // router to fan the message out. |
| // |
| while (dest_node_ref) { |
| if (dest_node_ref->router->next_hop) |
| dest_link = dest_node_ref->router->next_hop->peer_link; |
| else |
| dest_link = dest_node_ref->router->peer_link; |
| if (dest_link && qd_bitmask_value(dest_node_ref->router->valid_origins, origin)) |
| qd_bitmask_set_bit(link_set, dest_link->mask_bit); |
| dest_node_ref = DEQ_NEXT(dest_node_ref); |
| } |
| |
| // |
| // Send a copy of the message outbound on each identified link. |
| // |
| int link_bit; |
| while (qd_bitmask_first_set(link_set, &link_bit)) { |
| qd_bitmask_clear_bit(link_set, link_bit); |
| dest_link = router->out_links_by_mask_bit[link_bit]; |
| if (dest_link) { |
| qd_routed_event_t *re = new_qd_routed_event_t(); |
| DEQ_ITEM_INIT(re); |
| re->delivery = 0; |
| re->message = qd_message_copy(msg); |
| re->settle = 0; |
| re->disposition = 0; |
| DEQ_INSERT_TAIL(dest_link->msg_fifo, re); |
| |
| fanout++; |
| if (fanout == 1 && !qd_delivery_settled(delivery)) { |
| re->delivery = delivery; |
| qd_delivery_fifo_enter_LH(delivery); |
| } |
| |
| addr->deliveries_transit++; |
| qd_link_activate(dest_link->link); |
| } |
| } |
| |
| qd_bitmask_free(link_set); |
| } |
| } |
| } |
| } |
| |
| // |
| // In message-routing mode, the handling of the incoming delivery depends on the |
| // number of copies of the received message that were forwarded. |
| // |
| if (handler) { |
| qd_delivery_free_LH(delivery, PN_ACCEPTED); |
| } else if (fanout == 0) { |
| qd_delivery_free_LH(delivery, PN_RELEASED); |
| } else if (qd_delivery_settled(delivery)) { |
| qd_delivery_free_LH(delivery, 0); |
| } |
| } |
| } else { |
| // |
| // Message is invalid. Reject the message. |
| // |
| qd_delivery_free_LH(delivery, PN_REJECTED); |
| } |
| |
| sys_mutex_unlock(router->lock); |
| qd_message_free(msg); |
| |
| // |
| // Invoke the in-process handler now that the lock is released. |
| // |
| if (handler) { |
| handler(handler_context, in_process_copy, rlink->mask_bit); |
| qd_message_free(in_process_copy); |
| } |
| } |
| |
| |
| /** |
| * Delivery Disposition Handler |
| */ |
| static void router_disp_handler(void* context, qd_link_t *link, qd_delivery_t *delivery) |
| { |
| qd_router_t *router = (qd_router_t*) context; |
| bool changed = qd_delivery_disp_changed(delivery); |
| uint64_t disp = qd_delivery_disp(delivery); |
| bool settled = qd_delivery_settled(delivery); |
| |
| sys_mutex_lock(router->lock); |
| qd_delivery_t *peer = qd_delivery_peer(delivery); |
| if (peer) { |
| // |
| // The case where this delivery has a peer. |
| // |
| if (changed || settled) { |
| qd_link_t *peer_link = qd_delivery_link(peer); |
| qd_router_link_t *prl = (qd_router_link_t*) qd_link_get_context(peer_link); |
| qd_routed_event_t *re = new_qd_routed_event_t(); |
| DEQ_ITEM_INIT(re); |
| re->delivery = peer; |
| re->message = 0; |
| re->settle = settled; |
| re->disposition = changed ? disp : 0; |
| |
| qd_delivery_fifo_enter_LH(peer); |
| DEQ_INSERT_TAIL(prl->event_fifo, re); |
| if (settled) { |
| qd_delivery_unlink_LH(delivery); |
| qd_delivery_free_LH(delivery, 0); |
| } |
| |
| qd_link_activate(peer_link); |
| } |
| } else if (settled) |
| qd_delivery_free_LH(delivery, 0); |
| |
| sys_mutex_unlock(router->lock); |
| } |
| |
| |
| /** |
| * New Incoming Link Handler |
| */ |
| static int router_incoming_link_handler(void* context, qd_link_t *link) |
| { |
| qd_router_t *router = (qd_router_t*) context; |
| pn_link_t *pn_link = qd_link_pn(link); |
| int is_router = qd_router_terminus_is_router(qd_link_remote_source(link)); |
| |
| if (is_router && !qd_router_connection_is_inter_router(qd_link_connection(link))) { |
| qd_log(module, LOG_WARNING, "Incoming link claims router capability but is not on an inter-router connection"); |
| pn_link_close(pn_link); |
| return 0; |
| } |
| |
| qd_router_link_t *rlink = new_qd_router_link_t(); |
| DEQ_ITEM_INIT(rlink); |
| rlink->link_type = is_router ? QD_LINK_ROUTER : QD_LINK_ENDPOINT; |
| rlink->link_direction = QD_INCOMING; |
| rlink->owning_addr = 0; |
| rlink->link = link; |
| rlink->connected_link = 0; |
| rlink->peer_link = 0; |
| rlink->ref = 0; |
| DEQ_INIT(rlink->event_fifo); |
| DEQ_INIT(rlink->msg_fifo); |
| |
| qd_link_set_context(link, rlink); |
| |
| sys_mutex_lock(router->lock); |
| rlink->mask_bit = is_router ? qd_router_find_mask_bit_LH(router, link) : 0; |
| DEQ_INSERT_TAIL(router->links, rlink); |
| sys_mutex_unlock(router->lock); |
| |
| pn_terminus_copy(qd_link_source(link), qd_link_remote_source(link)); |
| pn_terminus_copy(qd_link_target(link), qd_link_remote_target(link)); |
| pn_link_flow(pn_link, 1000); |
| pn_link_open(pn_link); |
| |
| // |
| // TODO - If the address has link-route semantics, create all associated |
| // links needed to go with this one. |
| // |
| |
| return 0; |
| } |
| |
| |
| /** |
| * New Outgoing Link Handler |
| */ |
| static int router_outgoing_link_handler(void* context, qd_link_t *link) |
| { |
| qd_router_t *router = (qd_router_t*) context; |
| pn_link_t *pn_link = qd_link_pn(link); |
| const char *r_src = pn_terminus_get_address(qd_link_remote_source(link)); |
| int is_dynamic = pn_terminus_is_dynamic(qd_link_remote_source(link)); |
| int is_router = qd_router_terminus_is_router(qd_link_remote_target(link)); |
| int propagate = 0; |
| qd_field_iterator_t *iter = 0; |
| |
| if (is_router && !qd_router_connection_is_inter_router(qd_link_connection(link))) { |
| qd_log(module, LOG_WARNING, "Outgoing link claims router capability but is not on an inter-router connection"); |
| pn_link_close(pn_link); |
| return 0; |
| } |
| |
| // |
| // If this link is not a router link and it has no source address, we can't |
| // accept it. |
| // |
| if (r_src == 0 && !is_router && !is_dynamic) { |
| pn_link_close(pn_link); |
| return 0; |
| } |
| |
| |
| // |
| // If this is an endpoint link with a source address, make sure the address is |
| // appropriate for endpoint links. If it is not mobile address, it cannot be |
| // bound to an endpoint link. |
| // |
| if(r_src && !is_router && !is_dynamic) { |
| iter = qd_field_iterator_string(r_src, ITER_VIEW_ADDRESS_HASH); |
| unsigned char prefix = qd_field_iterator_octet(iter); |
| qd_field_iterator_reset(iter); |
| |
| if (prefix != 'M') { |
| qd_field_iterator_free(iter); |
| pn_link_close(pn_link); |
| qd_log(module, LOG_WARNING, "Rejected an outgoing endpoint link with a router address: %s", r_src); |
| return 0; |
| } |
| } |
| |
| // |
| // Create a router_link record for this link. Some of the fields will be |
| // modified in the different cases below. |
| // |
| qd_router_link_t *rlink = new_qd_router_link_t(); |
| DEQ_ITEM_INIT(rlink); |
| rlink->link_type = is_router ? QD_LINK_ROUTER : QD_LINK_ENDPOINT; |
| rlink->link_direction = QD_OUTGOING; |
| rlink->owning_addr = 0; |
| rlink->link = link; |
| rlink->connected_link = 0; |
| rlink->peer_link = 0; |
| rlink->ref = 0; |
| DEQ_INIT(rlink->event_fifo); |
| DEQ_INIT(rlink->msg_fifo); |
| |
| qd_link_set_context(link, rlink); |
| pn_terminus_copy(qd_link_source(link), qd_link_remote_source(link)); |
| pn_terminus_copy(qd_link_target(link), qd_link_remote_target(link)); |
| |
| sys_mutex_lock(router->lock); |
| rlink->mask_bit = is_router ? qd_router_find_mask_bit_LH(router, link) : 0; |
| |
| if (is_router) { |
| // |
| // If this is a router link, put it in the hello_address link-list. |
| // |
| qd_router_add_link_ref_LH(&router->hello_addr->rlinks, rlink); |
| rlink->owning_addr = router->hello_addr; |
| router->out_links_by_mask_bit[rlink->mask_bit] = rlink; |
| |
| } else { |
| // |
| // If this is an endpoint link, check the source. If it is dynamic, we will |
| // assign it an ephemeral and routable address. If it has a non-dymanic |
| // address, that address needs to be set up in the address list. |
| // |
| char temp_addr[1000]; // FIXME |
| qd_address_t *addr; |
| |
| if (is_dynamic) { |
| qd_router_generate_temp_addr(router, temp_addr, 1000); |
| iter = qd_field_iterator_string(temp_addr, ITER_VIEW_ADDRESS_HASH); |
| pn_terminus_set_address(qd_link_source(link), temp_addr); |
| qd_log(module, LOG_INFO, "Assigned temporary routable address: %s", temp_addr); |
| } else |
| qd_log(module, LOG_INFO, "Registered local address: %s", r_src); |
| |
| qd_hash_retrieve(router->addr_hash, iter, (void**) &addr); |
| if (!addr) { |
| addr = new_qd_address_t(); |
| memset(addr, 0, sizeof(qd_address_t)); |
| DEQ_ITEM_INIT(addr); |
| DEQ_INIT(addr->rlinks); |
| DEQ_INIT(addr->rnodes); |
| addr->semantics = &default_semantics; // FIXME - Use provisioned address here |
| qd_hash_insert(router->addr_hash, iter, addr, &addr->hash_handle); |
| DEQ_INSERT_TAIL(router->addrs, addr); |
| } |
| |
| rlink->owning_addr = addr; |
| qd_router_add_link_ref_LH(&addr->rlinks, rlink); |
| |
| // |
| // If this is not a dynamic address and it is the first local subscription |
| // to the address, supply the address to the router module for propagation |
| // to other nodes. |
| // |
| propagate = (!is_dynamic) && (DEQ_SIZE(addr->rlinks) == 1); |
| } |
| |
| DEQ_INSERT_TAIL(router->links, rlink); |
| sys_mutex_unlock(router->lock); |
| |
| if (propagate) |
| qd_router_mobile_added(router, iter); |
| |
| if (iter) |
| qd_field_iterator_free(iter); |
| pn_link_open(pn_link); |
| return 0; |
| } |
| |
| |
| /** |
| * Link Detached Handler |
| */ |
| static int router_link_detach_handler(void* context, qd_link_t *link, int closed) |
| { |
| qd_router_t *router = (qd_router_t*) context; |
| qd_router_link_t *rlink = (qd_router_link_t*) qd_link_get_context(link); |
| qd_router_conn_t *shared = (qd_router_conn_t*) qd_link_get_conn_context(link); |
| qd_address_t *oaddr = 0; |
| |
| if (shared) { |
| qd_link_set_conn_context(link, 0); |
| free_qd_router_conn_t(shared); |
| } |
| |
| if (!rlink) |
| return 0; |
| |
| sys_mutex_lock(router->lock); |
| |
| // |
| // If the link is outgoing, we must disassociate it from its address. |
| // |
| if (rlink->link_direction == QD_OUTGOING && rlink->owning_addr) { |
| qd_router_del_link_ref_LH(&rlink->owning_addr->rlinks, rlink); |
| oaddr = rlink->owning_addr; |
| } |
| |
| // |
| // If this is an outgoing inter-router link, we must remove the by-mask-bit |
| // index reference to this link. |
| // |
| if (rlink->link_type == QD_LINK_ROUTER && rlink->link_direction == QD_OUTGOING) { |
| if (router->out_links_by_mask_bit[rlink->mask_bit] == rlink) |
| router->out_links_by_mask_bit[rlink->mask_bit] = 0; |
| else |
| qd_log(module, LOG_CRITICAL, "Outgoing router link closing but not in index: bit=%d", rlink->mask_bit); |
| } |
| |
| // |
| // If this is an incoming inter-router link, we must free the mask_bit. |
| // |
| if (rlink->link_type == QD_LINK_ROUTER && rlink->link_direction == QD_INCOMING) |
| qd_bitmask_set_bit(router->neighbor_free_mask, rlink->mask_bit); |
| |
| // |
| // Remove the link from the master list-of-links. |
| // |
| DEQ_REMOVE(router->links, rlink); |
| sys_mutex_unlock(router->lock); |
| |
| // |
| // Check to see if the owning address should be deleted |
| // |
| qd_router_check_addr(router, oaddr, 1); |
| |
| // TODO - wrap the free to handle the recursive items |
| free_qd_router_link_t(rlink); |
| |
| return 0; |
| } |
| |
| |
| static void router_inbound_open_handler(void *type_context, qd_connection_t *conn) |
| { |
| } |
| |
| |
| static void router_outbound_open_handler(void *type_context, qd_connection_t *conn) |
| { |
| // |
| // Check the configured role of this connection. If it is not the inter-router |
| // role, ignore it. |
| // |
| if (!qd_router_connection_is_inter_router(conn)) { |
| qd_log(module, LOG_WARNING, "Outbound connection set up without inter-router role"); |
| return; |
| } |
| |
| qd_router_t *router = (qd_router_t*) type_context; |
| qd_link_t *sender; |
| qd_link_t *receiver; |
| qd_router_link_t *rlink; |
| int mask_bit = 0; |
| size_t clen = strlen(QD_CAPABILITY_ROUTER); |
| |
| // |
| // Allocate a mask bit to designate the pair of links connected to the neighbor router |
| // |
| sys_mutex_lock(router->lock); |
| if (qd_bitmask_first_set(router->neighbor_free_mask, &mask_bit)) { |
| qd_bitmask_clear_bit(router->neighbor_free_mask, mask_bit); |
| } else { |
| sys_mutex_unlock(router->lock); |
| qd_log(module, LOG_CRITICAL, "Exceeded maximum inter-router link count"); |
| return; |
| } |
| |
| // |
| // Create an incoming link with router source capability |
| // |
| receiver = qd_link(router->node, conn, QD_INCOMING, QD_INTERNODE_LINK_NAME_1); |
| // TODO - We don't want to have to cast away the constness of the literal string here! |
| // See PROTON-429 |
| pn_data_put_symbol(pn_terminus_capabilities(qd_link_target(receiver)), pn_bytes(clen, (char*) QD_CAPABILITY_ROUTER)); |
| |
| rlink = new_qd_router_link_t(); |
| DEQ_ITEM_INIT(rlink); |
| rlink->mask_bit = mask_bit; |
| rlink->link_type = QD_LINK_ROUTER; |
| rlink->link_direction = QD_INCOMING; |
| rlink->owning_addr = 0; |
| rlink->link = receiver; |
| rlink->connected_link = 0; |
| rlink->peer_link = 0; |
| DEQ_INIT(rlink->event_fifo); |
| DEQ_INIT(rlink->msg_fifo); |
| |
| qd_link_set_context(receiver, rlink); |
| DEQ_INSERT_TAIL(router->links, rlink); |
| |
| // |
| // Create an outgoing link with router target capability |
| // |
| sender = qd_link(router->node, conn, QD_OUTGOING, QD_INTERNODE_LINK_NAME_2); |
| // TODO - We don't want to have to cast away the constness of the literal string here! |
| // See PROTON-429 |
| pn_data_put_symbol(pn_terminus_capabilities(qd_link_source(sender)), pn_bytes(clen, (char *) QD_CAPABILITY_ROUTER)); |
| |
| rlink = new_qd_router_link_t(); |
| DEQ_ITEM_INIT(rlink); |
| rlink->mask_bit = mask_bit; |
| rlink->link_type = QD_LINK_ROUTER; |
| rlink->link_direction = QD_OUTGOING; |
| rlink->owning_addr = router->hello_addr; |
| rlink->link = sender; |
| rlink->connected_link = 0; |
| rlink->peer_link = 0; |
| DEQ_INIT(rlink->event_fifo); |
| DEQ_INIT(rlink->msg_fifo); |
| |
| // |
| // Add the new outgoing link to the hello_address's list of links. |
| // |
| qd_router_add_link_ref_LH(&router->hello_addr->rlinks, rlink); |
| |
| // |
| // Index this link from the by-maskbit index so we can later find it quickly |
| // when provided with the mask bit. |
| // |
| router->out_links_by_mask_bit[mask_bit] = rlink; |
| |
| qd_link_set_context(sender, rlink); |
| DEQ_INSERT_TAIL(router->links, rlink); |
| sys_mutex_unlock(router->lock); |
| |
| pn_link_open(qd_link_pn(receiver)); |
| pn_link_open(qd_link_pn(sender)); |
| pn_link_flow(qd_link_pn(receiver), 1000); |
| } |
| |
| |
| static void qd_router_timer_handler(void *context) |
| { |
| qd_router_t *router = (qd_router_t*) context; |
| |
| // |
| // Periodic processing. |
| // |
| qd_pyrouter_tick(router); |
| qd_timer_schedule(router->timer, 1000); |
| } |
| |
| |
| static qd_node_type_t router_node = {"router", 0, 0, |
| router_rx_handler, |
| router_disp_handler, |
| router_incoming_link_handler, |
| router_outgoing_link_handler, |
| router_writable_link_handler, |
| router_link_detach_handler, |
| 0, // node_created_handler |
| 0, // node_destroyed_handler |
| router_inbound_open_handler, |
| router_outbound_open_handler }; |
| static int type_registered = 0; |
| |
| |
| qd_router_t *qd_router(qd_dispatch_t *qd, qd_router_mode_t mode, const char *area, const char *id) |
| { |
| if (!type_registered) { |
| type_registered = 1; |
| qd_container_register_node_type(qd, &router_node); |
| } |
| |
| size_t dplen = 9 + strlen(area) + strlen(id); |
| direct_prefix = (char*) malloc(dplen); |
| strcpy(direct_prefix, "_topo/"); |
| strcat(direct_prefix, area); |
| strcat(direct_prefix, "/"); |
| strcat(direct_prefix, id); |
| strcat(direct_prefix, "/"); |
| |
| node_id = (char*) malloc(dplen); |
| strcpy(node_id, area); |
| strcat(node_id, "/"); |
| strcat(node_id, id); |
| |
| qd_router_t *router = NEW(qd_router_t); |
| |
| router_node.type_context = router; |
| |
| qd->router = router; |
| router->qd = qd; |
| router->router_mode = mode; |
| router->router_area = area; |
| router->router_id = id; |
| router->node = qd_container_set_default_node_type(qd, &router_node, (void*) router, QD_DIST_BOTH); |
| DEQ_INIT(router->addrs); |
| router->addr_hash = qd_hash(10, 32, 0); |
| |
| DEQ_INIT(router->links); |
| DEQ_INIT(router->routers); |
| |
| router->out_links_by_mask_bit = NEW_PTR_ARRAY(qd_router_link_t, qd_bitmask_width()); |
| router->routers_by_mask_bit = NEW_PTR_ARRAY(qd_router_node_t, qd_bitmask_width()); |
| for (int idx = 0; idx < qd_bitmask_width(); idx++) { |
| router->out_links_by_mask_bit[idx] = 0; |
| router->routers_by_mask_bit[idx] = 0; |
| } |
| |
| router->neighbor_free_mask = qd_bitmask(1); |
| router->lock = sys_mutex(); |
| router->timer = qd_timer(qd, qd_router_timer_handler, (void*) router); |
| router->dtag = 1; |
| router->pyRouter = 0; |
| router->pyTick = 0; |
| router->pyAdded = 0; |
| router->pyRemoved = 0; |
| |
| // |
| // Create addresses for all of the routers in the topology. It will be registered |
| // locally later in the initialization sequence. |
| // |
| if (router->router_mode == QD_ROUTER_MODE_INTERIOR) { |
| router->router_addr = qd_router_register_address(qd, "qdrouter", 0, &router_semantics, 0); |
| router->hello_addr = qd_router_register_address(qd, "qdhello", 0, &router_semantics, 0); |
| } |
| |
| // |
| // Inform the field iterator module of this router's id and area. The field iterator |
| // uses this to offload some of the address-processing load from the router. |
| // |
| qd_field_iterator_set_address(area, id); |
| |
| // |
| // Set up the usage of the embedded python router module. |
| // |
| qd_python_start(); |
| |
| // |
| // Seed the random number generator |
| // |
| unsigned int seed = (unsigned int) time(0); |
| srandom(seed); |
| |
| switch (router->router_mode) { |
| case QD_ROUTER_MODE_STANDALONE: qd_log(module, LOG_INFO, "Router started in Standalone mode"); break; |
| case QD_ROUTER_MODE_INTERIOR: qd_log(module, LOG_INFO, "Router started in Interior mode, area=%s id=%s", area, id); break; |
| case QD_ROUTER_MODE_EDGE: qd_log(module, LOG_INFO, "Router started in Edge mode"); break; |
| } |
| |
| return router; |
| } |
| |
| |
| void qd_router_setup_late(qd_dispatch_t *qd) |
| { |
| qd_router_agent_setup(qd->router); |
| qd_router_python_setup(qd->router); |
| qd_timer_schedule(qd->router->timer, 1000); |
| } |
| |
| |
| void qd_router_free(qd_router_t *router) |
| { |
| qd_container_set_default_node_type(router->qd, 0, 0, QD_DIST_BOTH); |
| sys_mutex_free(router->lock); |
| free(router); |
| qd_python_stop(); |
| } |
| |
| |
| const char *qd_router_id(const qd_dispatch_t *qd) |
| { |
| return node_id; |
| } |
| |
| |
| qd_address_t *qd_router_register_address(qd_dispatch_t *qd, |
| const char *address, |
| qd_router_message_cb_t handler, |
| const qd_address_semantics_t *semantics, |
| void *context) |
| { |
| char addr_string[1000]; |
| qd_router_t *router = qd->router; |
| qd_address_t *addr; |
| qd_field_iterator_t *iter; |
| |
| strcpy(addr_string, "L"); // Local Hash-Key Space |
| strcat(addr_string, address); |
| iter = qd_field_iterator_string(addr_string, ITER_VIEW_NO_HOST); |
| |
| sys_mutex_lock(router->lock); |
| qd_hash_retrieve(router->addr_hash, iter, (void**) &addr); |
| if (!addr) { |
| addr = new_qd_address_t(); |
| memset(addr, 0, sizeof(qd_address_t)); |
| DEQ_ITEM_INIT(addr); |
| DEQ_INIT(addr->rlinks); |
| DEQ_INIT(addr->rnodes); |
| addr->semantics = semantics; |
| qd_hash_insert(router->addr_hash, iter, addr, &addr->hash_handle); |
| DEQ_ITEM_INIT(addr); |
| DEQ_INSERT_TAIL(router->addrs, addr); |
| } |
| qd_field_iterator_free(iter); |
| |
| addr->handler = handler; |
| addr->handler_context = context; |
| |
| sys_mutex_unlock(router->lock); |
| |
| if (handler) |
| qd_log(module, LOG_INFO, "In-Process Address Registered: %s", address); |
| return addr; |
| } |
| |
| |
| void qd_router_unregister_address(qd_address_t *ad) |
| { |
| //free_qd_address_t(ad); |
| } |
| |
| |
| void qd_router_send(qd_dispatch_t *qd, |
| qd_field_iterator_t *address, |
| qd_message_t *msg) |
| { |
| qd_router_t *router = qd->router; |
| qd_address_t *addr; |
| |
| qd_field_iterator_reset_view(address, ITER_VIEW_ADDRESS_HASH); |
| sys_mutex_lock(router->lock); |
| qd_hash_retrieve(router->addr_hash, address, (void*) &addr); |
| if (addr) { |
| // |
| // Forward to all of the local links receiving this address. |
| // |
| addr->deliveries_from_container++; |
| qd_router_link_ref_t *dest_link_ref = DEQ_HEAD(addr->rlinks); |
| while (dest_link_ref) { |
| qd_routed_event_t *re = new_qd_routed_event_t(); |
| DEQ_ITEM_INIT(re); |
| re->delivery = 0; |
| re->message = qd_message_copy(msg); |
| re->settle = 0; |
| re->disposition = 0; |
| DEQ_INSERT_TAIL(dest_link_ref->link->msg_fifo, re); |
| |
| qd_link_activate(dest_link_ref->link->link); |
| addr->deliveries_egress++; |
| |
| dest_link_ref = DEQ_NEXT(dest_link_ref); |
| } |
| |
| // |
| // Forward to the next-hops for remote destinations. |
| // |
| qd_router_ref_t *dest_node_ref = DEQ_HEAD(addr->rnodes); |
| qd_router_link_t *dest_link; |
| qd_bitmask_t *link_set = qd_bitmask(0); |
| |
| while (dest_node_ref) { |
| if (dest_node_ref->router->next_hop) |
| dest_link = dest_node_ref->router->next_hop->peer_link; |
| else |
| dest_link = dest_node_ref->router->peer_link; |
| if (dest_link) |
| qd_bitmask_set_bit(link_set, dest_link->mask_bit); |
| dest_node_ref = DEQ_NEXT(dest_node_ref); |
| } |
| |
| int link_bit; |
| while (qd_bitmask_first_set(link_set, &link_bit)) { |
| qd_bitmask_clear_bit(link_set, link_bit); |
| dest_link = router->out_links_by_mask_bit[link_bit]; |
| if (dest_link) { |
| qd_routed_event_t *re = new_qd_routed_event_t(); |
| DEQ_ITEM_INIT(re); |
| re->delivery = 0; |
| re->message = qd_message_copy(msg); |
| re->settle = 0; |
| re->disposition = 0; |
| DEQ_INSERT_TAIL(dest_link->msg_fifo, re); |
| qd_link_activate(dest_link->link); |
| addr->deliveries_transit++; |
| } |
| } |
| |
| qd_bitmask_free(link_set); |
| } |
| sys_mutex_unlock(router->lock); // TOINVESTIGATE Move this higher? |
| } |
| |
| |
| void qd_router_send2(qd_dispatch_t *qd, |
| const char *address, |
| qd_message_t *msg) |
| { |
| qd_field_iterator_t *iter = qd_field_iterator_string(address, ITER_VIEW_ADDRESS_HASH); |
| qd_router_send(qd, iter, msg); |
| qd_field_iterator_free(iter); |
| } |
| |