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