Bug 9060: mdsal-trace tooling with getAllUnique() to find Tx leaks
[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.util.ArrayList;
11 import java.util.HashMap;
12 import java.util.List;
13 import java.util.Map;
14 import java.util.Objects;
15 import javax.annotation.Nonnull;
16
17 import org.opendaylight.controller.md.sal.common.api.data.AsyncDataBroker;
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.DOMDataBroker;
21 import org.opendaylight.controller.md.sal.dom.api.DOMDataBrokerExtension;
22 import org.opendaylight.controller.md.sal.dom.api.DOMDataChangeListener;
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.CloseTrackedRegistry;
32 import org.opendaylight.mdsal.binding.dom.codec.api.BindingNormalizedNodeSerializer;
33 import org.opendaylight.yang.gen.v1.urn.opendaylight.params.xml.ns.yang.mdsaltrace.rev160908.Config;
34 import org.opendaylight.yangtools.concepts.ListenerRegistration;
35 import org.opendaylight.yangtools.yang.binding.DataObject;
36 import org.opendaylight.yangtools.yang.binding.InstanceIdentifier;
37 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier;
38 import org.slf4j.Logger;
39 import org.slf4j.LoggerFactory;
40
41 @SuppressWarnings("checkstyle:JavadocStyle")
42 //...because otherwise it whines about the elements in the @code block even though it's completely valid Javadoc
43
44 /**
45  * TracingBroker logs "write" operations and listener registrations to the md-sal. It logs the instance identifier path,
46  * the objects themselves, as well as the stack trace of the call invoking the registration or write operation.
47  * It works by operating as a "bump on the stack" between the application and actual DataBroker, intercepting write
48  * and registration calls and writing to the log.
49  *
50  * <p>In addition, it (optionally) can also keep track of the stack trace of all new transaction allocations
51  * (including TransactionChains, and transactions created in turn from them), in order to detect and report leaks
52  *  results from transactions which were not closed.
53  *
54  * <h1>Wiring:</h1>
55  * TracingBroker is designed to be easy to use. In fact, for bundles using Blueprint to inject their DataBroker
56  * TracingBroker can be used without modifying your code at all in two simple steps:
57  * <ol>
58  * <li>
59  * Simply add the dependency "mdsaltrace-features" to
60  * your Karaf pom:
61  * <pre>
62  * {@code
63  *  <dependency>
64  *    <groupId>org.opendaylight.controller</groupId>
65  *    <artifactId>mdsal-trace-features</artifactId>
66  *    <classifier>features</classifier>
67  *    <type>xml</type>
68  *    <scope>runtime</scope>
69  *    <version>0.1.6-SNAPSHOT</version>
70  *  </dependency>
71  * }
72  * </pre>
73  * </li>
74  * <li>
75  * Then just load the odl-mdsal-trace feature before your feature and you're done.
76  * </li>
77  * </ol>
78  * This works because the mdsaltrace-impl bundle registers its service implementing DOMDataBroker with a higher
79  * rank than sal-binding-broker. As such, any OSGi service lookup for DataBroker will receive the TracingBroker.
80  * <p> </p>
81  * <h1>Avoiding log bloat:</h1>
82  * TracingBroker can be configured to only print registrations or write ops pertaining to certain subtrees of the
83  * md-sal. This can be done in the code via the methods of this class or via a config file. TracingBroker uses a more
84  * convenient but non-standard representation of the instance identifiers. Each instance identifier segment's
85  * class.getSimpleName() is used separated by a '/'.
86  * <p> </p>
87  * <h1>Known issues</h1>
88  * <ul>
89  *     <li>
90  *        Filtering by paths. For some registrations the codec that converts back from the DOM to binding paths is
91  *        busted. As such, an aproximated path is used in the output. For now it is recommended not to use
92  *        watchRegistrations and allow all registrations to be logged.
93  *     </li>
94  * </ul>
95  *
96  */
97 public class TracingBroker implements TracingDOMDataBroker {
98
99     static final Logger LOG = LoggerFactory.getLogger(TracingBroker.class);
100
101     private static final int STACK_TRACE_FIRST_RELEVANT_FRAME = 2;
102
103     private final BindingNormalizedNodeSerializer codec;
104     private final DOMDataBroker delegate;
105     private final List<Watch> registrationWatches = new ArrayList<>();
106     private final List<Watch> writeWatches = new ArrayList<>();
107
108     private final boolean isDebugging;
109     private final CloseTrackedRegistry<TracingTransactionChain> transactionChainsRegistry;
110     private final CloseTrackedRegistry<TracingReadOnlyTransaction> readOnlyTransactionsRegistry;
111     private final CloseTrackedRegistry<TracingWriteTransaction> writeTransactionsRegistry;
112     private final CloseTrackedRegistry<TracingReadWriteTransaction> readWriteTransactionsRegistry;
113
114     private class Watch {
115         final String iidString;
116         final LogicalDatastoreType store;
117
118         Watch(String iidString, LogicalDatastoreType storeOrNull) {
119             this.store = storeOrNull;
120             this.iidString = iidString;
121         }
122
123         private String toIidCompString(YangInstanceIdentifier iid) {
124             StringBuilder builder = new StringBuilder();
125             toPathString(iid, builder);
126             builder.append('/');
127             return builder.toString();
128         }
129
130         private boolean isParent(String parent, String child) {
131             int parentOffset = 0;
132             if (parent.length() > 0 && parent.charAt(0) == '<') {
133                 parentOffset = parent.indexOf('>') + 1;
134             }
135
136             int childOffset = 0;
137             if (child.length() > 0 && child.charAt(0) == '<') {
138                 childOffset = child.indexOf('>') + 1;
139             }
140
141             return child.startsWith(parent.substring(parentOffset), childOffset);
142         }
143
144         public boolean subtreesOverlap(YangInstanceIdentifier iid, LogicalDatastoreType store,
145                                                                 AsyncDataBroker.DataChangeScope scope) {
146             if (this.store != null && !this.store.equals(store)) {
147                 return false;
148             }
149
150             String otherIidString = toIidCompString(iid);
151             switch (scope) {
152                 case BASE:
153                     return isParent(iidString, otherIidString);
154                 case ONE: //for now just treat like SUBTREE, even though it's not
155                 case SUBTREE:
156                     return isParent(iidString, otherIidString) || isParent(otherIidString, iidString);
157                 default:
158                     return false;
159             }
160         }
161
162         public boolean eventIsOfInterest(YangInstanceIdentifier iid, LogicalDatastoreType store) {
163             if (this.store != null && !this.store.equals(store)) {
164                 return false;
165             }
166
167             return isParent(iidString, toPathString(iid));
168         }
169     }
170
171     public TracingBroker(DOMDataBroker delegate, Config config, BindingNormalizedNodeSerializer codec) {
172         this.delegate = Objects.requireNonNull(delegate);
173         this.codec = Objects.requireNonNull(codec);
174         configure(config);
175
176         if (config.isTransactionDebugContextEnabled() != null) {
177             this.isDebugging = config.isTransactionDebugContextEnabled();
178         } else {
179             this.isDebugging = false;
180         }
181         this.transactionChainsRegistry     = new CloseTrackedRegistry<>(this, "createTransactionChain", isDebugging);
182         this.readOnlyTransactionsRegistry  = new CloseTrackedRegistry<>(this, "newReadOnlyTransaction", isDebugging);
183         this.writeTransactionsRegistry     = new CloseTrackedRegistry<>(this, "newWriteOnlyTransaction", isDebugging);
184         this.readWriteTransactionsRegistry = new CloseTrackedRegistry<>(this, "newReadWriteTransaction", isDebugging);
185     }
186
187     private void configure(Config config) {
188         registrationWatches.clear();
189         List<String> paths = config.getRegistrationWatches();
190         if (paths != null) {
191             for (String path : paths) {
192                 watchRegistrations(path, null);
193             }
194         }
195
196         writeWatches.clear();
197         paths = config.getWriteWatches();
198         if (paths != null) {
199             for (String path : paths) {
200                 watchWrites(path, null);
201             }
202         }
203     }
204
205     /**
206      * Log registrations to this subtree of the md-sal.
207      * @param iidString the iid path of the root of the subtree
208      * @param store Which LogicalDataStore? or null for both
209      */
210     public void watchRegistrations(String iidString, LogicalDatastoreType store) {
211         LOG.info("Watching registrations to {} in {}", iidString, store);
212         registrationWatches.add(new Watch(iidString, store));
213     }
214
215     /**
216      * Log writes to this subtree of the md-sal.
217      * @param iidString the iid path of the root of the subtree
218      * @param store Which LogicalDataStore? or null for both
219      */
220     public void watchWrites(String iidString, LogicalDatastoreType store) {
221         LOG.info("Watching writes to {} in {}", iidString, store);
222         Watch watch = new Watch(iidString, store);
223         writeWatches.add(watch);
224     }
225
226     private boolean isRegistrationWatched(YangInstanceIdentifier iid,
227                                                             LogicalDatastoreType store, DataChangeScope scope) {
228         if (registrationWatches.isEmpty()) {
229             return true;
230         }
231
232         for (Watch regInterest : registrationWatches) {
233             if (regInterest.subtreesOverlap(iid, store, scope)) {
234                 return true;
235             }
236         }
237
238         return false;
239     }
240
241     boolean isWriteWatched(YangInstanceIdentifier iid, LogicalDatastoreType store) {
242         if (writeWatches.isEmpty()) {
243             return true;
244         }
245
246         for (Watch watch : writeWatches) {
247             if (watch.eventIsOfInterest(iid, store)) {
248                 return true;
249             }
250         }
251
252         return false;
253     }
254
255     static void toPathString(InstanceIdentifier<? extends DataObject> iid, StringBuilder builder) {
256         for (InstanceIdentifier.PathArgument pathArg : iid.getPathArguments()) {
257             builder.append('/').append(pathArg.getType().getSimpleName());
258         }
259     }
260
261     String toPathString(YangInstanceIdentifier  yiid) {
262         StringBuilder sb = new StringBuilder();
263         toPathString(yiid, sb);
264         return sb.toString();
265     }
266
267
268     private void toPathString(YangInstanceIdentifier yiid, StringBuilder sb) {
269         InstanceIdentifier<?> iid = codec.fromYangInstanceIdentifier(yiid);
270         if (null == iid) {
271             reconstructIidPathString(yiid, sb);
272         } else {
273             toPathString(iid, sb);
274         }
275     }
276
277     private void reconstructIidPathString(YangInstanceIdentifier yiid, StringBuilder sb) {
278         sb.append("<RECONSTRUCTED FROM: \"").append(yiid.toString()).append("\">");
279         for (YangInstanceIdentifier.PathArgument pathArg : yiid.getPathArguments()) {
280             if (pathArg instanceof YangInstanceIdentifier.AugmentationIdentifier) {
281                 sb.append('/').append("AUGMENTATION");
282                 continue;
283             }
284             sb.append('/').append(pathArg.getNodeType().getLocalName());
285         }
286     }
287
288     String getStackSummary() {
289         StackTraceElement[] stack = Thread.currentThread().getStackTrace();
290
291         StringBuilder sb = new StringBuilder();
292         for (int i = STACK_TRACE_FIRST_RELEVANT_FRAME; i < stack.length; i++) {
293             StackTraceElement frame = stack[i];
294             sb.append("\n\t(TracingBroker)\t").append(frame.getClassName()).append('.').append(frame.getMethodName());
295         }
296
297         return sb.toString();
298     }
299
300     @Override
301     public DOMDataReadWriteTransaction newReadWriteTransaction() {
302         return new TracingReadWriteTransaction(delegate.newReadWriteTransaction(), this, readWriteTransactionsRegistry);
303     }
304
305     @Override
306     public DOMDataWriteTransaction newWriteOnlyTransaction() {
307         return new TracingWriteTransaction(delegate.newWriteOnlyTransaction(), this, writeTransactionsRegistry);
308     }
309
310     @Override
311     public ListenerRegistration<DOMDataChangeListener> registerDataChangeListener(
312                                                         LogicalDatastoreType store, YangInstanceIdentifier yiid,
313                                                         DOMDataChangeListener listener, DataChangeScope scope) {
314         if (isRegistrationWatched(yiid, store, scope)) {
315             LOG.warn("Registration (registerDataChangeListener) for {} from {}",
316                     toPathString(yiid), getStackSummary());
317         }
318         return delegate.registerDataChangeListener(store, yiid, listener, scope);
319     }
320
321     @Override
322     public DOMTransactionChain createTransactionChain(TransactionChainListener transactionChainListener) {
323         return new TracingTransactionChain(
324                 delegate.createTransactionChain(transactionChainListener), this, transactionChainsRegistry);
325     }
326
327     @Override
328     public DOMDataReadOnlyTransaction newReadOnlyTransaction() {
329         return new TracingReadOnlyTransaction(delegate.newReadOnlyTransaction(), this, readOnlyTransactionsRegistry);
330     }
331
332     @Nonnull
333     @Override
334     public Map<Class<? extends DOMDataBrokerExtension>, DOMDataBrokerExtension> getSupportedExtensions() {
335         Map<Class<? extends DOMDataBrokerExtension>, DOMDataBrokerExtension> res = delegate.getSupportedExtensions();
336         DOMDataTreeChangeService treeChangeSvc = (DOMDataTreeChangeService) res.get(DOMDataTreeChangeService.class);
337         if (treeChangeSvc == null) {
338             return res;
339         }
340
341         res = new HashMap<>(res);
342
343         res.put(DOMDataTreeChangeService.class, new DOMDataTreeChangeService() {
344             @Nonnull
345             @Override
346             public <L extends DOMDataTreeChangeListener> ListenerRegistration<L> registerDataTreeChangeListener(
347                     @Nonnull DOMDataTreeIdentifier domDataTreeIdentifier, @Nonnull L listener) {
348                 if (isRegistrationWatched(domDataTreeIdentifier.getRootIdentifier(),
349                         domDataTreeIdentifier.getDatastoreType(), DataChangeScope.SUBTREE)) {
350                     LOG.warn("Registration (registerDataTreeChangeListener) for {} from {}",
351                             toPathString(domDataTreeIdentifier.getRootIdentifier()), getStackSummary());
352                 }
353                 return treeChangeSvc.registerDataTreeChangeListener(domDataTreeIdentifier, listener);
354             }
355         });
356
357         return res;
358     }
359 }