5b90ff2696aaa42e99f95646ff1ee81ae08cb9b6
[bgpcep.git] / bgp / rib-impl / src / main / java / org / opendaylight / protocol / bgp / rib / impl / AdjRibInWriter.java
1 /*
2  * Copyright (c) 2015 Cisco Systems, Inc. and others.  All rights reserved.
3  *
4  * This program and the accompanying materials are made available under the
5  * terms of the Eclipse Public License v1.0 which accompanies this distribution,
6  * and is available at http://www.eclipse.org/legal/epl-v10.html
7  */
8 package org.opendaylight.protocol.bgp.rib.impl;
9
10 import static java.util.Objects.requireNonNull;
11 import static org.opendaylight.protocol.bgp.rib.spi.RIBNodeIdentifiers.ADJRIBIN;
12 import static org.opendaylight.protocol.bgp.rib.spi.RIBNodeIdentifiers.ADJRIBOUT;
13 import static org.opendaylight.protocol.bgp.rib.spi.RIBNodeIdentifiers.ATTRIBUTES;
14 import static org.opendaylight.protocol.bgp.rib.spi.RIBNodeIdentifiers.EFFRIBIN;
15 import static org.opendaylight.protocol.bgp.rib.spi.RIBNodeIdentifiers.TABLES;
16
17 import com.google.common.annotations.VisibleForTesting;
18 import com.google.common.base.Optional;
19 import com.google.common.collect.ImmutableMap;
20 import com.google.common.collect.ImmutableMap.Builder;
21 import com.google.common.util.concurrent.FluentFuture;
22 import com.google.common.util.concurrent.FutureCallback;
23 import com.google.common.util.concurrent.Futures;
24 import com.google.common.util.concurrent.MoreExecutors;
25 import java.util.Collection;
26 import java.util.Collections;
27 import java.util.HashMap;
28 import java.util.List;
29 import java.util.Locale;
30 import java.util.Map;
31 import java.util.Map.Entry;
32 import java.util.Set;
33 import java.util.concurrent.CountDownLatch;
34 import java.util.concurrent.ExecutionException;
35 import java.util.stream.Collectors;
36 import javax.annotation.Nonnull;
37 import javax.annotation.Nullable;
38 import javax.annotation.concurrent.GuardedBy;
39 import javax.annotation.concurrent.NotThreadSafe;
40 import org.opendaylight.controller.md.sal.common.api.data.LogicalDatastoreType;
41 import org.opendaylight.controller.md.sal.dom.api.DOMDataReadOnlyTransaction;
42 import org.opendaylight.controller.md.sal.dom.api.DOMDataWriteTransaction;
43 import org.opendaylight.mdsal.common.api.CommitInfo;
44 import org.opendaylight.protocol.bgp.rib.impl.ApplicationPeer.RegisterAppPeerListener;
45 import org.opendaylight.protocol.bgp.rib.impl.spi.PeerTransactionChain;
46 import org.opendaylight.protocol.bgp.rib.impl.spi.RIBSupportContext;
47 import org.opendaylight.protocol.bgp.rib.impl.spi.RIBSupportContextRegistry;
48 import org.opendaylight.protocol.bgp.rib.spi.IdentifierUtils;
49 import org.opendaylight.protocol.bgp.rib.spi.PeerRoleUtil;
50 import org.opendaylight.protocol.bgp.rib.spi.RibSupportUtils;
51 import org.opendaylight.yang.gen.v1.urn.opendaylight.params.xml.ns.yang.bgp.multiprotocol.rev180329.SendReceive;
52 import org.opendaylight.yang.gen.v1.urn.opendaylight.params.xml.ns.yang.bgp.multiprotocol.rev180329.update.attributes.MpReachNlri;
53 import org.opendaylight.yang.gen.v1.urn.opendaylight.params.xml.ns.yang.bgp.multiprotocol.rev180329.update.attributes.MpUnreachNlri;
54 import org.opendaylight.yang.gen.v1.urn.opendaylight.params.xml.ns.yang.bgp.rib.rev180329.PeerId;
55 import org.opendaylight.yang.gen.v1.urn.opendaylight.params.xml.ns.yang.bgp.rib.rev180329.PeerRole;
56 import org.opendaylight.yang.gen.v1.urn.opendaylight.params.xml.ns.yang.bgp.rib.rev180329.bgp.rib.rib.Peer;
57 import org.opendaylight.yang.gen.v1.urn.opendaylight.params.xml.ns.yang.bgp.rib.rev180329.bgp.rib.rib.peer.SupportedTables;
58 import org.opendaylight.yang.gen.v1.urn.opendaylight.params.xml.ns.yang.bgp.rib.rev180329.rib.TablesKey;
59 import org.opendaylight.yang.gen.v1.urn.opendaylight.params.xml.ns.yang.bgp.rib.rev180329.rib.tables.Attributes;
60 import org.opendaylight.yangtools.yang.common.QName;
61 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier;
62 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.InstanceIdentifierBuilder;
63 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.NodeIdentifier;
64 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.NodeIdentifierWithPredicates;
65 import org.opendaylight.yangtools.yang.data.api.schema.ContainerNode;
66 import org.opendaylight.yangtools.yang.data.api.schema.LeafNode;
67 import org.opendaylight.yangtools.yang.data.api.schema.MapEntryNode;
68 import org.opendaylight.yangtools.yang.data.api.schema.MapNode;
69 import org.opendaylight.yangtools.yang.data.api.schema.NormalizedNode;
70 import org.opendaylight.yangtools.yang.data.impl.schema.Builders;
71 import org.opendaylight.yangtools.yang.data.impl.schema.ImmutableNodes;
72 import org.opendaylight.yangtools.yang.data.impl.schema.builder.api.DataContainerNodeAttrBuilder;
73 import org.opendaylight.yangtools.yang.data.impl.schema.builder.api.DataContainerNodeBuilder;
74 import org.opendaylight.yangtools.yang.data.impl.schema.builder.impl.ImmutableMapNodeBuilder;
75 import org.slf4j.Logger;
76 import org.slf4j.LoggerFactory;
77
78 /**
79  * Writer of Adjacency-RIB-In for a single peer. An instance of this object
80  * is attached to each {@link BGPPeer} and {@link ApplicationPeer}.
81  */
82 @NotThreadSafe
83 final class AdjRibInWriter {
84
85     private static final Logger LOG = LoggerFactory.getLogger(AdjRibInWriter.class);
86
87     @VisibleForTesting
88     static final LeafNode<Boolean> ATTRIBUTES_UPTODATE_FALSE = ImmutableNodes.leafNode(QName.create(Attributes.QNAME,
89             "uptodate"), Boolean.FALSE);
90     @VisibleForTesting
91     static final QName PEER_ID_QNAME = QName.create(Peer.QNAME, "peer-id").intern();
92     private static final LeafNode<Boolean> ATTRIBUTES_UPTODATE_TRUE =
93             ImmutableNodes.leafNode(ATTRIBUTES_UPTODATE_FALSE.getNodeType(), Boolean.TRUE);
94     private static final QName PEER_ROLE_QNAME = QName.create(Peer.QNAME, "peer-role").intern();
95     private static final NodeIdentifier PEER_ID = NodeIdentifier.create(PEER_ID_QNAME);
96     private static final NodeIdentifier PEER_ROLE = NodeIdentifier.create(PEER_ROLE_QNAME);
97     private static final NodeIdentifier PEER_TABLES = NodeIdentifier.create(SupportedTables.QNAME);
98     private static final QName SEND_RECEIVE = QName.create(SupportedTables.QNAME, "send-receive").intern();
99
100     // FIXME: is there a utility method to construct this?
101     private static final MapNode EMPTY_TABLES = ImmutableNodes.mapNodeBuilder(TABLES).build();
102     private static final ContainerNode EMPTY_ADJRIBIN = Builders.containerBuilder()
103             .withNodeIdentifier(ADJRIBIN).addChild(EMPTY_TABLES).build();
104     private static final ContainerNode EMPTY_EFFRIBIN = Builders.containerBuilder()
105             .withNodeIdentifier(EFFRIBIN).addChild(EMPTY_TABLES).build();
106     private static final ContainerNode EMPTY_ADJRIBOUT = Builders.containerBuilder()
107             .withNodeIdentifier(ADJRIBOUT).addChild(EMPTY_TABLES).build();
108
109     private final Map<TablesKey, TableContext> tables;
110     private final YangInstanceIdentifier ribPath;
111     private final PeerTransactionChain chain;
112     private final PeerRole role;
113     @GuardedBy("this")
114     private final Map<TablesKey, Collection<NodeIdentifierWithPredicates>> staleRoutesRegistry = new HashMap<>();
115     @GuardedBy("this")
116     private FluentFuture<? extends CommitInfo> submitted;
117
118     private AdjRibInWriter(final YangInstanceIdentifier ribPath, final PeerTransactionChain chain, final PeerRole role,
119             final Map<TablesKey, TableContext> tables) {
120         this.ribPath = requireNonNull(ribPath);
121         this.chain = requireNonNull(chain);
122         this.tables = requireNonNull(tables);
123         this.role = requireNonNull(role);
124     }
125
126     /**
127      * Create a new writer using a transaction chain.
128      *
129      * @param role                peer's role
130      * @param chain               transaction chain  @return A fresh writer instance
131      */
132     static AdjRibInWriter create(@Nonnull final YangInstanceIdentifier ribId, @Nonnull final PeerRole role,
133             @Nonnull final PeerTransactionChain chain) {
134         return new AdjRibInWriter(ribId, chain, role, Collections.emptyMap());
135     }
136
137     /**
138      * Transform this writer to a new writer, which is in charge of specified tables.
139      * Empty tables are created for new entries and old tables are deleted. Once this
140      * method returns, the old instance must not be reasonably used.
141      *
142      * @param newPeerId         new peer BGP identifier
143      * @param peerPath          path of the peer in the datastore
144      * @param registry          RIB extension registry
145      * @param tableTypes        New tables, must not be null
146      * @param addPathTablesType supported add path tables
147      * @return New writer
148      */
149     AdjRibInWriter transform(final PeerId newPeerId, final YangInstanceIdentifier peerPath,
150             final RIBSupportContextRegistry registry,
151             final Set<TablesKey> tableTypes, final Map<TablesKey, SendReceive> addPathTablesType) {
152         return transform(newPeerId, peerPath, registry, tableTypes, addPathTablesType, null);
153     }
154
155     AdjRibInWriter transform(final PeerId newPeerId, final YangInstanceIdentifier peerPath,
156             final RIBSupportContextRegistry registry, final Set<TablesKey> tableTypes,
157             final Map<TablesKey, SendReceive> addPathTablesType,
158             @Nullable final RegisterAppPeerListener registerAppPeerListener) {
159         final DOMDataWriteTransaction tx = this.chain.getDomChain().newWriteOnlyTransaction();
160
161         createEmptyPeerStructure(newPeerId, peerPath, tx);
162         final ImmutableMap<TablesKey, TableContext> tb = createNewTableInstances(peerPath, registry, tableTypes,
163                 addPathTablesType, tx);
164
165         tx.commit().addCallback(new FutureCallback<CommitInfo>() {
166             @Override
167             public void onSuccess(final CommitInfo result) {
168                 if (registerAppPeerListener != null) {
169                     LOG.trace("Application Peer Listener registered");
170                     registerAppPeerListener.register();
171                 }
172             }
173
174             @Override
175             public void onFailure(final Throwable throwable) {
176                 if (registerAppPeerListener != null) {
177                     LOG.error("Failed to create Empty Structure, Application Peer Listener won't be registered",
178                             throwable);
179                 } else {
180                     LOG.error("Failed to create Empty Structure", throwable);
181                 }
182             }
183         }, MoreExecutors.directExecutor());
184         return new AdjRibInWriter(this.ribPath, this.chain, this.role, tb);
185     }
186
187     /**
188      * Create new table instances, potentially creating their empty entries.
189      */
190     private static ImmutableMap<TablesKey, TableContext> createNewTableInstances(
191             final YangInstanceIdentifier newPeerPath, final RIBSupportContextRegistry registry,
192             final Set<TablesKey> tableTypes, final Map<TablesKey, SendReceive> addPathTablesType,
193             final DOMDataWriteTransaction tx) {
194
195         final Builder<TablesKey, TableContext> tb = ImmutableMap.builder();
196         for (final TablesKey tableKey : tableTypes) {
197             final RIBSupportContext rs = registry.getRIBSupportContext(tableKey);
198             // TODO: Use returned value once Instance Identifier builder allows for it.
199             final NodeIdentifierWithPredicates instanceIdentifierKey = RibSupportUtils.toYangTablesKey(tableKey);
200             if (rs == null) {
201                 LOG.warn("No support for table type {}, skipping it", tableKey);
202                 continue;
203             }
204             installAdjRibsOutTables(newPeerPath, rs, instanceIdentifierKey, tableKey,
205                     addPathTablesType.get(tableKey), tx);
206             installAdjRibInTables(newPeerPath, tableKey, rs, instanceIdentifierKey, tx, tb);
207         }
208         return tb.build();
209     }
210
211     private static void installAdjRibInTables(final YangInstanceIdentifier newPeerPath, final TablesKey tableKey,
212             final RIBSupportContext rs, final NodeIdentifierWithPredicates instanceIdentifierKey,
213             final DOMDataWriteTransaction tx, final Builder<TablesKey, TableContext> tb) {
214         // We will use table keys very often, make sure they are optimized
215         final InstanceIdentifierBuilder idb = YangInstanceIdentifier.builder(newPeerPath
216                 .node(EMPTY_ADJRIBIN.getIdentifier()).node(TABLES));
217         idb.nodeWithKey(instanceIdentifierKey.getNodeType(), instanceIdentifierKey.getKeyValues());
218
219         final TableContext ctx = new TableContext(rs, idb.build());
220         ctx.createEmptyTableStructure(tx);
221
222         tx.merge(LogicalDatastoreType.OPERATIONAL, ctx.getTableId().node(ATTRIBUTES)
223                 .node(ATTRIBUTES_UPTODATE_FALSE.getNodeType()), ATTRIBUTES_UPTODATE_FALSE);
224         LOG.debug("Created table instance {}", ctx.getTableId());
225         tb.put(tableKey, ctx);
226     }
227
228     private static void installAdjRibsOutTables(final YangInstanceIdentifier newPeerPath, final RIBSupportContext rs,
229             final NodeIdentifierWithPredicates instanceIdentifierKey, final TablesKey tableKey,
230             final SendReceive sendReceive, final DOMDataWriteTransaction tx) {
231         final NodeIdentifierWithPredicates supTablesKey = RibSupportUtils.toYangKey(SupportedTables.QNAME, tableKey);
232         final DataContainerNodeAttrBuilder<NodeIdentifierWithPredicates, MapEntryNode> tt =
233                 Builders.mapEntryBuilder().withNodeIdentifier(supTablesKey);
234         for (final Entry<QName, Object> e : supTablesKey.getKeyValues().entrySet()) {
235             tt.withChild(ImmutableNodes.leafNode(e.getKey(), e.getValue()));
236         }
237         if (sendReceive != null) {
238             tt.withChild(ImmutableNodes.leafNode(SEND_RECEIVE, sendReceive.toString().toLowerCase(Locale.ENGLISH)));
239         }
240         tx.put(LogicalDatastoreType.OPERATIONAL, newPeerPath.node(PEER_TABLES).node(supTablesKey), tt.build());
241         rs.createEmptyTableStructure(tx, newPeerPath.node(EMPTY_ADJRIBOUT.getIdentifier())
242                 .node(TABLES).node(instanceIdentifierKey));
243     }
244
245     private void createEmptyPeerStructure(final PeerId newPeerId,
246             final YangInstanceIdentifier peerPath, final DOMDataWriteTransaction tx) {
247         final NodeIdentifierWithPredicates peerKey = IdentifierUtils.domPeerId(newPeerId);
248
249         tx.put(LogicalDatastoreType.OPERATIONAL, peerPath, peerSkeleton(peerKey, newPeerId.getValue()));
250         LOG.debug("New peer {} structure installed.", peerPath);
251     }
252
253     @VisibleForTesting
254     MapEntryNode peerSkeleton(final NodeIdentifierWithPredicates peerKey, final String peerId) {
255         final DataContainerNodeBuilder<NodeIdentifierWithPredicates, MapEntryNode> pb = Builders.mapEntryBuilder();
256         pb.withNodeIdentifier(peerKey);
257         pb.withChild(ImmutableNodes.leafNode(PEER_ID, peerId));
258         pb.withChild(ImmutableNodes.leafNode(PEER_ROLE, PeerRoleUtil.roleForString(this.role)));
259         pb.withChild(ImmutableMapNodeBuilder.create().withNodeIdentifier(PEER_TABLES).build());
260         pb.withChild(EMPTY_ADJRIBIN);
261         pb.withChild(EMPTY_EFFRIBIN);
262         pb.withChild(EMPTY_ADJRIBOUT);
263         return pb.build();
264     }
265
266     void markTableUptodate(final TablesKey tableTypes) {
267         final DOMDataWriteTransaction tx = this.chain.getDomChain().newWriteOnlyTransaction();
268         final TableContext ctx = this.tables.get(tableTypes);
269         tx.merge(LogicalDatastoreType.OPERATIONAL, ctx.getTableId().node(ATTRIBUTES)
270                 .node(ATTRIBUTES_UPTODATE_TRUE.getNodeType()), ATTRIBUTES_UPTODATE_TRUE);
271         tx.commit().addCallback(new FutureCallback<CommitInfo>() {
272             @Override
273             public void onSuccess(final CommitInfo result) {
274                 LOG.trace("Write Attributes uptodate, succeed");
275             }
276
277             @Override
278             public void onFailure(final Throwable throwable) {
279                 LOG.error("Write Attributes uptodate failed", throwable);
280             }
281         }, MoreExecutors.directExecutor());
282     }
283
284     void updateRoutes(final MpReachNlri nlri, final org.opendaylight.yang.gen.v1.urn.opendaylight.params
285             .xml.ns.yang.bgp.message.rev180329.path.attributes.Attributes attributes) {
286         final TablesKey key = new TablesKey(nlri.getAfi(), nlri.getSafi());
287         final TableContext ctx = this.tables.get(key);
288         if (ctx == null) {
289             LOG.debug("No table for {}, not accepting NLRI {}", key, nlri);
290             return;
291         }
292
293         final DOMDataWriteTransaction tx = this.chain.getDomChain().newWriteOnlyTransaction();
294         final Collection<NodeIdentifierWithPredicates> routeKeys = ctx.writeRoutes(tx, nlri, attributes);
295         final Collection<NodeIdentifierWithPredicates> staleRoutes = this.staleRoutesRegistry.get(key);
296         if (staleRoutes != null) {
297             staleRoutes.removeAll(routeKeys);
298         }
299         LOG.trace("Write routes {}", nlri);
300         final FluentFuture<? extends CommitInfo> future = tx.commit();
301         this.submitted = future;
302         future.addCallback(new FutureCallback<CommitInfo>() {
303             @Override
304             public void onSuccess(final CommitInfo result) {
305                 LOG.trace("Write routes {}, succeed", nlri);
306             }
307
308             @Override
309             public void onFailure(final Throwable throwable) {
310                 LOG.error("Write routes failed", throwable);
311             }
312         }, MoreExecutors.directExecutor());
313     }
314
315     void removeRoutes(final MpUnreachNlri nlri) {
316         final TablesKey key = new TablesKey(nlri.getAfi(), nlri.getSafi());
317         final TableContext ctx = this.tables.get(key);
318         if (ctx == null) {
319             LOG.debug("No table for {}, not accepting NLRI {}", key, nlri);
320             return;
321         }
322         LOG.trace("Removing routes {}", nlri);
323         final DOMDataWriteTransaction tx = this.chain.getDomChain().newWriteOnlyTransaction();
324         ctx.removeRoutes(tx, nlri);
325         final FluentFuture<? extends CommitInfo> future = tx.commit();
326         this.submitted = future;
327         future.addCallback(new FutureCallback<CommitInfo>() {
328             @Override
329             public void onSuccess(final CommitInfo result) {
330                 LOG.trace("Removing routes {}, succeed", nlri);
331             }
332
333             @Override
334             public void onFailure(final Throwable throwable) {
335                 LOG.error("Removing routes failed", throwable);
336             }
337         }, MoreExecutors.directExecutor());
338     }
339
340     void releaseChain() {
341         if (this.submitted != null) {
342             try {
343                 this.submitted.get();
344             } catch (final InterruptedException | ExecutionException throwable) {
345                 LOG.error("Write routes failed", throwable);
346             }
347         }
348     }
349
350     void storeStaleRoutes(final Set<TablesKey> gracefulTables) {
351         final CountDownLatch latch = new CountDownLatch(gracefulTables.size());
352
353         try (DOMDataReadOnlyTransaction tx = this.chain.getDomChain().newReadOnlyTransaction()) {
354             for (TablesKey tablesKey : gracefulTables) {
355                 final TableContext ctx = this.tables.get(tablesKey);
356                 if (ctx == null) {
357                     LOG.warn("Missing table for address family {}", tablesKey);
358                     latch.countDown();
359                     continue;
360                 }
361
362                 Futures.addCallback(tx.read(LogicalDatastoreType.OPERATIONAL, ctx.routesPath()),
363                     new FutureCallback<Optional<NormalizedNode<?, ?>>>() {
364                         @Override
365                         public void onSuccess(final Optional<NormalizedNode<?, ?>> routesOptional) {
366                             try {
367                                 if (routesOptional.isPresent()) {
368                                     synchronized (AdjRibInWriter.this.staleRoutesRegistry) {
369                                         final MapNode routesNode = (MapNode) routesOptional.get();
370                                         final List<NodeIdentifierWithPredicates> routes = routesNode.getValue().stream()
371                                                 .map(MapEntryNode::getIdentifier)
372                                                 .collect(Collectors.toList());
373                                         if (!routes.isEmpty()) {
374                                             AdjRibInWriter.this.staleRoutesRegistry.put(tablesKey, routes);
375                                         }
376                                     }
377                                 }
378                             } finally {
379                                 latch.countDown();
380                             }
381                         }
382
383                         @Override
384                         public void onFailure(final Throwable throwable) {
385                             LOG.warn("Failed to store stale routes for table {}", tablesKey, throwable);
386                             latch.countDown();
387                         }
388                     }, MoreExecutors.directExecutor());
389             }
390         }
391
392         try {
393             latch.await();
394         } catch (InterruptedException e) {
395             LOG.warn("Interrupted while waiting to store stale routes with {} tasks of {} to finish", latch.getCount(),
396                 gracefulTables, e);
397         }
398     }
399
400     void removeStaleRoutes(final TablesKey tableKey) {
401         final TableContext ctx = this.tables.get(tableKey);
402         if (ctx == null) {
403             LOG.debug("No table for {}, not removing any stale routes", tableKey);
404             return;
405         }
406         final Collection<NodeIdentifierWithPredicates> routeKeys = this.staleRoutesRegistry.get(tableKey);
407         if (routeKeys == null || routeKeys.isEmpty()) {
408             LOG.debug("No stale routes present in table {}", tableKey);
409             return;
410         }
411         LOG.trace("Removing routes {}", routeKeys);
412         final DOMDataWriteTransaction tx = this.chain.getDomChain().newWriteOnlyTransaction();
413         routeKeys.forEach(routeKey -> {
414             tx.delete(LogicalDatastoreType.OPERATIONAL, ctx.routePath(routeKey));
415         });
416         final FluentFuture<? extends CommitInfo> future = tx.commit();
417         this.submitted = future;
418         future.addCallback(new FutureCallback<CommitInfo>() {
419             @Override
420             public void onSuccess(final CommitInfo result) {
421                 LOG.trace("Removing routes {}, succeed", routeKeys);
422                 synchronized (AdjRibInWriter.this.staleRoutesRegistry) {
423                     staleRoutesRegistry.remove(tableKey);
424                 }
425             }
426
427             @Override
428             public void onFailure(final Throwable throwable) {
429                 LOG.warn("Removing routes {}, failed", routeKeys, throwable);
430             }
431         }, MoreExecutors.directExecutor());
432     }
433
434     FluentFuture<? extends CommitInfo> clearTables(final Set<TablesKey> tablesToClear) {
435         if (tablesToClear == null || tablesToClear.isEmpty()) {
436             return CommitInfo.emptyFluentFuture();
437         }
438
439         final DOMDataWriteTransaction wtx = this.chain.getDomChain().newWriteOnlyTransaction();
440         tablesToClear.forEach(tableKey -> {
441             final TableContext ctx = this.tables.get(tableKey);
442             wtx.delete(LogicalDatastoreType.OPERATIONAL, ctx.routesPath().getParent());
443         });
444         return wtx.commit();
445     }
446 }