Deprecate Clustered(DOM)DataTreeChangeListener
[mdsal.git] / trace / mdsal-trace-impl / src / main / java / org / opendaylight / mdsal / trace / impl / TracingBroker.java
1 /*
2  * Copyright (c) 2016 Red Hat, 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.mdsal.trace.impl;
9
10 import static java.util.Objects.requireNonNull;
11
12 import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
13 import java.io.PrintStream;
14 import java.util.ArrayList;
15 import java.util.List;
16 import java.util.Set;
17 import org.opendaylight.mdsal.binding.dom.codec.api.BindingCodecTree;
18 import org.opendaylight.mdsal.common.api.LogicalDatastoreType;
19 import org.opendaylight.mdsal.dom.api.DOMDataBroker;
20 import org.opendaylight.mdsal.dom.api.DOMDataTreeChangeListener;
21 import org.opendaylight.mdsal.dom.api.DOMDataTreeIdentifier;
22 import org.opendaylight.mdsal.dom.api.DOMDataTreeReadTransaction;
23 import org.opendaylight.mdsal.dom.api.DOMDataTreeReadWriteTransaction;
24 import org.opendaylight.mdsal.dom.api.DOMDataTreeWriteTransaction;
25 import org.opendaylight.mdsal.dom.api.DOMTransactionChain;
26 import org.opendaylight.mdsal.trace.api.TracingDOMDataBroker;
27 import org.opendaylight.yang.gen.v1.urn.opendaylight.params.xml.ns.yang.mdsaltrace.rev160908.Config;
28 import org.opendaylight.yangtools.concepts.Registration;
29 import org.opendaylight.yangtools.yang.binding.DataObject;
30 import org.opendaylight.yangtools.yang.binding.InstanceIdentifier;
31 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier;
32 import org.slf4j.Logger;
33 import org.slf4j.LoggerFactory;
34
35 @SuppressWarnings("checkstyle:JavadocStyle")
36 //...because otherwise it whines about the elements in the @code block even though it's completely valid Javadoc
37 /**
38  * TracingBroker logs "write" operations and listener registrations to the md-sal. It logs the instance identifier path,
39  * the objects themselves, as well as the stack trace of the call invoking the registration or write operation.
40  * It works by operating as a "bump on the stack" between the application and actual DataBroker, intercepting write
41  * and registration calls and writing to the log.
42  *
43  * <p>In addition, it (optionally) can also keep track of the stack trace of all new transaction allocations
44  * (including TransactionChains, and transactions created in turn from them), in order to detect and report leaks
45  * from transactions which were not closed.
46  *
47  * <h1>Wiring:</h1>
48  * TracingBroker is designed to be easy to use. In fact, for bundles using Blueprint to inject their DataBroker
49  * TracingBroker can be used without modifying your code at all in two simple steps:
50  * <ol>
51  * <li>
52  * Simply add the dependency "mdsaltrace-features" to
53  * your Karaf pom:
54  * <pre>
55  * {@code
56  *  <dependency>
57  *    <groupId>org.opendaylight.controller</groupId>
58  *    <artifactId>features-mdsal-trace</artifactId>
59  *    <version>1.7.0-SNAPSHOT</version>
60  *    <classifier>features</classifier>
61  *    <type>xml</type>
62  *    <scope>runtime</scope>
63  *  </dependency>
64  * }
65  * </pre>
66  * </li>
67  * <li>
68  * Then just "feature:install odl-mdsal-trace" before you install your "real" feature(s) and you're done.
69  * Beware that with Karaf 4 due to <a href="https://bugs.opendaylight.org/show_bug.cgi?id=9068">Bug 9068</a>
70  * you'll probably have to use feature:install's --no-auto-refresh flag when installing your "real" feature.
71  * </li>
72  * </ol>
73  * This works because the mdsaltrace-impl bundle registers its service implementing DOMDataBroker with a higher
74  * rank than sal-binding-broker. As such, any OSGi service lookup for DataBroker will receive the TracingBroker.
75  * <p> </p>
76  * <h1>Avoiding log bloat:</h1>
77  * TracingBroker can be configured to only print registrations or write ops pertaining to certain subtrees of the
78  * md-sal. This can be done in the code via the methods of this class or via a config file. TracingBroker uses a more
79  * convenient but non-standard representation of the instance identifiers. Each instance identifier segment's
80  * class.getSimpleName() is used separated by a '/'.
81  * <p> </p>
82  * <h1>Known issues</h1>
83  * <ul>
84  *     <li>
85  *        Filtering by paths. For some registrations the codec that converts back from the DOM to binding paths is
86  *        busted. As such, an aproximated path is used in the output. For now it is recommended not to use
87  *        watchRegistrations and allow all registrations to be logged.
88  *     </li>
89  * </ul>
90  */
91 public class TracingBroker implements TracingDOMDataBroker {
92     private static final Logger LOG = LoggerFactory.getLogger(TracingBroker.class);
93
94     private static final int STACK_TRACE_FIRST_RELEVANT_FRAME = 2;
95
96     private final BindingCodecTree codec;
97     private final DOMDataBroker delegate;
98     private final List<Watch> registrationWatches = new ArrayList<>();
99     private final List<Watch> writeWatches = new ArrayList<>();
100
101     private final boolean isDebugging;
102     private final CloseTrackedRegistry<TracingTransactionChain> transactionChainsRegistry;
103     private final CloseTrackedRegistry<TracingReadOnlyTransaction> readOnlyTransactionsRegistry;
104     private final CloseTrackedRegistry<TracingWriteTransaction> writeTransactionsRegistry;
105     private final CloseTrackedRegistry<TracingReadWriteTransaction> readWriteTransactionsRegistry;
106
107     private class Watch {
108         final String iidString;
109         final LogicalDatastoreType store;
110
111         Watch(final String iidString, final LogicalDatastoreType storeOrNull) {
112             store = storeOrNull;
113             this.iidString = iidString;
114         }
115
116         private String toIidCompString(final YangInstanceIdentifier iid) {
117             StringBuilder builder = new StringBuilder();
118             toPathString(iid, builder);
119             return builder.append('/').toString();
120         }
121
122         private boolean isParent(final String parent, final String child) {
123             int parentOffset = 0;
124             if (parent.length() > 0 && parent.charAt(0) == '<') {
125                 parentOffset = parent.indexOf('>') + 1;
126             }
127
128             int childOffset = 0;
129             if (child.length() > 0 && child.charAt(0) == '<') {
130                 childOffset = child.indexOf('>') + 1;
131             }
132
133             return child.startsWith(parent.substring(parentOffset), childOffset);
134         }
135
136         @SuppressWarnings({ "checkstyle:hiddenField", "hiding" })
137         public boolean subtreesOverlap(final YangInstanceIdentifier iid, final LogicalDatastoreType store) {
138             if (this.store != null && !this.store.equals(store)) {
139                 return false;
140             }
141
142             String otherIidString = toIidCompString(iid);
143             return isParent(iidString, otherIidString) || isParent(otherIidString, iidString);
144         }
145
146         @SuppressWarnings({ "checkstyle:hiddenField", "hiding" })
147         public boolean eventIsOfInterest(final YangInstanceIdentifier iid, final LogicalDatastoreType store) {
148             if (this.store != null && !this.store.equals(store)) {
149                 return false;
150             }
151
152             return isParent(iidString, toPathString(iid));
153         }
154     }
155
156     public TracingBroker(final DOMDataBroker delegate, final Config config, final BindingCodecTree codec) {
157         this.delegate = requireNonNull(delegate, "delegate");
158         this.codec = requireNonNull(codec, "codec");
159         configure(config);
160
161         isDebugging = Boolean.TRUE.equals(config.getTransactionDebugContextEnabled());
162         final String db = "DataBroker";
163         transactionChainsRegistry     = new CloseTrackedRegistry<>(db, "createTransactionChain()", isDebugging);
164         readOnlyTransactionsRegistry  = new CloseTrackedRegistry<>(db, "newReadOnlyTransaction()", isDebugging);
165         writeTransactionsRegistry     = new CloseTrackedRegistry<>(db, "newWriteOnlyTransaction()", isDebugging);
166         readWriteTransactionsRegistry = new CloseTrackedRegistry<>(db, "newReadWriteTransaction()", isDebugging);
167     }
168
169     private void configure(final Config config) {
170         registrationWatches.clear();
171         Set<String> paths = config.getRegistrationWatches();
172         if (paths != null) {
173             for (String path : paths) {
174                 watchRegistrations(path, null);
175             }
176         }
177
178         writeWatches.clear();
179         paths = config.getWriteWatches();
180         if (paths != null) {
181             for (String path : paths) {
182                 watchWrites(path, null);
183             }
184         }
185     }
186
187     /**
188      * Log registrations to this subtree of the md-sal.
189      * @param iidString the iid path of the root of the subtree
190      * @param store Which LogicalDataStore? or null for both
191      */
192     public void watchRegistrations(final String iidString, final LogicalDatastoreType store) {
193         LOG.info("Watching registrations to {} in {}", iidString, store);
194         registrationWatches.add(new Watch(iidString, store));
195     }
196
197     /**
198      * Log writes to this subtree of the md-sal.
199      * @param iidString the iid path of the root of the subtree
200      * @param store Which LogicalDataStore? or null for both
201      */
202     public void watchWrites(final String iidString, final LogicalDatastoreType store) {
203         LOG.info("Watching writes to {} in {}", iidString, store);
204         Watch watch = new Watch(iidString, store);
205         writeWatches.add(watch);
206     }
207
208     private boolean isRegistrationWatched(final YangInstanceIdentifier iid, final LogicalDatastoreType store) {
209         if (registrationWatches.isEmpty()) {
210             return true;
211         }
212
213         for (Watch regInterest : registrationWatches) {
214             if (regInterest.subtreesOverlap(iid, store)) {
215                 return true;
216             }
217         }
218
219         return false;
220     }
221
222     boolean isWriteWatched(final YangInstanceIdentifier iid, final LogicalDatastoreType store) {
223         if (writeWatches.isEmpty()) {
224             return true;
225         }
226
227         for (Watch watch : writeWatches) {
228             if (watch.eventIsOfInterest(iid, store)) {
229                 return true;
230             }
231         }
232
233         return false;
234     }
235
236     static void toPathString(final InstanceIdentifier<? extends DataObject> iid, final StringBuilder builder) {
237         for (var pathArg : iid.getPathArguments()) {
238             builder.append('/').append(pathArg.type().getSimpleName());
239         }
240     }
241
242     String toPathString(final YangInstanceIdentifier  yiid) {
243         StringBuilder sb = new StringBuilder();
244         toPathString(yiid, sb);
245         return sb.toString();
246     }
247
248
249     private void toPathString(final YangInstanceIdentifier yiid, final StringBuilder sb) {
250         InstanceIdentifier<?> iid = codec.getInstanceIdentifierCodec().toBinding(yiid);
251         if (null == iid) {
252             reconstructIidPathString(yiid, sb);
253         } else {
254             toPathString(iid, sb);
255         }
256     }
257
258     private static void reconstructIidPathString(final YangInstanceIdentifier yiid, final StringBuilder sb) {
259         sb.append("<RECONSTRUCTED FROM: \"").append(yiid.toString()).append("\">");
260         for (YangInstanceIdentifier.PathArgument pathArg : yiid.getPathArguments()) {
261             sb.append('/').append(pathArg.getNodeType().getLocalName());
262         }
263     }
264
265     String getStackSummary() {
266         StackTraceElement[] stack = Thread.currentThread().getStackTrace();
267
268         StringBuilder sb = new StringBuilder();
269         for (int i = STACK_TRACE_FIRST_RELEVANT_FRAME; i < stack.length; i++) {
270             StackTraceElement frame = stack[i];
271             sb.append("\n\t(TracingBroker)\t").append(frame.getClassName()).append('.').append(frame.getMethodName());
272         }
273
274         return sb.toString();
275     }
276
277     @Override
278     public DOMDataTreeReadWriteTransaction newReadWriteTransaction() {
279         return new TracingReadWriteTransaction(delegate.newReadWriteTransaction(), this, readWriteTransactionsRegistry);
280     }
281
282     @Override
283     public DOMDataTreeWriteTransaction newWriteOnlyTransaction() {
284         return new TracingWriteTransaction(delegate.newWriteOnlyTransaction(), this, writeTransactionsRegistry);
285     }
286
287     @Override
288     public DOMTransactionChain createTransactionChain() {
289         return new TracingTransactionChain(delegate.createTransactionChain(), this, transactionChainsRegistry);
290     }
291
292     @Override
293     public DOMTransactionChain createMergingTransactionChain() {
294         return new TracingTransactionChain(delegate.createMergingTransactionChain(), this, transactionChainsRegistry);
295     }
296
297     @Override
298     public DOMDataTreeReadTransaction newReadOnlyTransaction() {
299         return new TracingReadOnlyTransaction(delegate.newReadOnlyTransaction(), readOnlyTransactionsRegistry);
300     }
301
302     @Override
303     public <T extends Extension> T extension(final Class<T> type) {
304         final var ext = delegate.extension(type);
305         if (DataTreeChangeExtension.class.equals(type) && ext instanceof DataTreeChangeExtension treeChange) {
306             return type.cast(new DataTreeChangeExtension() {
307                 @Override
308                 public Registration registerTreeChangeListener(final DOMDataTreeIdentifier treeId,
309                         final DOMDataTreeChangeListener listener) {
310                     notifyIfWatched("Non-clustered", treeId, listener);
311                     return treeChange.registerTreeChangeListener(treeId, listener);
312                 }
313
314                 @Override
315                 @Deprecated(since = "13.0.0", forRemoval = true)
316                 public Registration registerLegacyTreeChangeListener(final DOMDataTreeIdentifier treeId,
317                         final DOMDataTreeChangeListener listener) {
318                     notifyIfWatched("Non-clustered", treeId, listener);
319                     return treeChange.registerLegacyTreeChangeListener(treeId, listener);
320                 }
321
322                 private void notifyIfWatched(final String kind, final DOMDataTreeIdentifier treeId,
323                         final DOMDataTreeChangeListener listener) {
324                     final var rootId = treeId.path();
325                     if (isRegistrationWatched(rootId, treeId.datastore()) && LOG.isWarnEnabled()) {
326                         LOG.warn("{} registration (registerDataTreeChangeListener) for {} from {}.", kind,
327                             toPathString(rootId), getStackSummary());
328                     }
329                 }
330             });
331         }
332         return ext;
333     }
334
335     @Override
336     public boolean printOpenTransactions(final PrintStream ps, final int minOpenTXs) {
337         if (transactionChainsRegistry.getAllUnique().isEmpty()
338             && readOnlyTransactionsRegistry.getAllUnique().isEmpty()
339             && writeTransactionsRegistry.getAllUnique().isEmpty()
340             && readWriteTransactionsRegistry.getAllUnique().isEmpty()) {
341
342             ps.println("No open transactions, great!");
343             return false;
344         }
345
346         ps.println(getClass().getSimpleName() + " found some not yet (or never..) closed transaction[chain]s!");
347         ps.println("[NB: If no stack traces are shown below, then "
348                  + "enable transaction-debug-context-enabled in mdsaltrace_config.xml]");
349         ps.println();
350         // Flag to track if we really found any real leaks with more (or equal) to minOpenTXs
351         boolean hasFound = print(readOnlyTransactionsRegistry, ps, "  ", minOpenTXs);
352         hasFound |= print(writeTransactionsRegistry, ps, "  ", minOpenTXs);
353         hasFound |= print(readWriteTransactionsRegistry, ps, "  ", minOpenTXs);
354
355         // Now print details for each non-closed TransactionChain
356         // incl. in turn each ones own read/Write[Only]TransactionsRegistry
357         Set<CloseTrackedRegistryReportEntry<TracingTransactionChain>>
358             entries = transactionChainsRegistry.getAllUnique();
359         if (!entries.isEmpty()) {
360             ps.println("  " + transactionChainsRegistry.getAnchor() + " : "
361                     + transactionChainsRegistry.getCreateDescription());
362         }
363         for (CloseTrackedRegistryReportEntry<TracingTransactionChain> entry : entries) {
364             ps.println("    " + entry.getNumberAddedNotRemoved() + "x TransactionChains opened but not closed here:");
365             printStackTraceElements(ps, "      ", entry.getStackTraceElements());
366             @SuppressWarnings("resource")
367             TracingTransactionChain txChain = (TracingTransactionChain) entry
368                 .getExampleCloseTracked().getRealCloseTracked();
369             hasFound |= print(txChain.getReadOnlyTransactionsRegistry(), ps, "        ", minOpenTXs);
370             hasFound |= print(txChain.getWriteTransactionsRegistry(), ps, "        ", minOpenTXs);
371             hasFound |= print(txChain.getReadWriteTransactionsRegistry(), ps, "        ", minOpenTXs);
372         }
373         ps.println();
374
375         return hasFound;
376     }
377
378     final void logEmptySet(final YangInstanceIdentifier yiid) {
379         if (LOG.isDebugEnabled()) {
380             LOG.debug("Empty data set write to {}", toPathString(yiid));
381         }
382     }
383
384     @SuppressFBWarnings(value = "SLF4J_SIGN_ONLY_FORMAT", justification = "pre-formatted logs")
385     static final void logOperations(final Object identifier, final List<?> operations) {
386         if (LOG.isWarnEnabled()) {
387             LOG.warn("Transaction {} contains the following operations:", identifier);
388             for (var operation : operations) {
389                 LOG.warn("{}", operation);
390             }
391         }
392     }
393
394     private <T extends CloseTracked<T>> boolean print(final CloseTrackedRegistry<T> registry, final PrintStream ps,
395             final String indent, final int minOpenTransactions) {
396         Set<CloseTrackedRegistryReportEntry<T>> unsorted = registry.getAllUnique();
397         if (unsorted.size() < minOpenTransactions) {
398             return false;
399         }
400
401         List<CloseTrackedRegistryReportEntry<T>> entries = new ArrayList<>(unsorted);
402         entries.sort((o1, o2) -> Long.compare(o2.getNumberAddedNotRemoved(), o1.getNumberAddedNotRemoved()));
403
404         if (!entries.isEmpty()) {
405             ps.println(indent + registry.getAnchor() + " : " + registry.getCreateDescription());
406         }
407         entries.forEach(entry -> {
408             ps.println(indent + "  " + entry.getNumberAddedNotRemoved()
409                 + "x transactions opened here, which are not closed:");
410             printStackTraceElements(ps, indent + "    ", entry.getStackTraceElements());
411         });
412         if (!entries.isEmpty()) {
413             ps.println();
414         }
415         return true;
416     }
417
418     private void printStackTraceElements(final PrintStream ps, final String indent,
419             final List<StackTraceElement> stackTraceElements) {
420         boolean ellipsis = false;
421         for (final StackTraceElement stackTraceElement : stackTraceElements) {
422             if (isStackTraceElementInteresting(stackTraceElement)) {
423                 ps.println(indent + stackTraceElement);
424                 ellipsis = false;
425             } else if (!ellipsis) {
426                 ps.println(indent + "(...)");
427                 ellipsis = true;
428             }
429         }
430     }
431
432     private boolean isStackTraceElementInteresting(final StackTraceElement element) {
433         final String className = element.getClassName();
434         return !className.startsWith(getClass().getPackage().getName())
435             && !className.startsWith(CloseTracked.class.getPackage().getName())
436             && !className.startsWith("Proxy")
437             && !className.startsWith("akka")
438             && !className.startsWith("scala")
439             && !className.startsWith("sun.reflect")
440             && !className.startsWith("java.lang.reflect")
441             && !className.startsWith("org.apache.aries.blueprint")
442             && !className.startsWith("org.osgi.util.tracker");
443     }
444 }