xref: /aosp_15_r20/external/aws-crt-java/src/main/java/software/amazon/awssdk/crt/CrtResource.java (revision 3c7ae9de214676c52d19f01067dc1a404272dc11)
1 /**
2  * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3  * SPDX-License-Identifier: Apache-2.0.
4  */
5 package software.amazon.awssdk.crt;
6 
7 import software.amazon.awssdk.crt.io.ClientBootstrap;
8 import software.amazon.awssdk.crt.io.EventLoopGroup;
9 import software.amazon.awssdk.crt.io.HostResolver;
10 
11 import java.time.Instant;
12 import java.util.ArrayList;
13 import java.util.concurrent.atomic.AtomicInteger;
14 import java.util.concurrent.atomic.AtomicLong;
15 import java.util.concurrent.locks.Condition;
16 import java.util.concurrent.locks.Lock;
17 import java.util.concurrent.locks.ReentrantLock;
18 import java.util.concurrent.TimeUnit;
19 import java.util.function.Consumer;
20 import java.util.HashMap;
21 import java.util.Map;
22 
23 
24 
25 /**
26  * This wraps a native pointer and/or one or more references to an AWS Common Runtime resource. It also ensures
27  * that the first time a resource is referenced, the CRT will be loaded and bound.
28  */
29 public abstract class CrtResource implements AutoCloseable {
30     private static final String NATIVE_DEBUG_PROPERTY_NAME = "aws.crt.debugnative";
31     private static final int DEBUG_CLEANUP_WAIT_TIME_IN_SECONDS = 60;
32     private static final long NULL = 0;
33 
34     private static final Log.LogLevel ResourceLogLevel = Log.LogLevel.Debug;
35 
36     /**
37      * Debug/diagnostic data about a CrtResource object
38      */
39     public class ResourceInstance {
40         public long nativeHandle;
41         public final String canonicalName;
42         private Throwable instantiation;
43         private CrtResource wrapper;
44 
ResourceInstance(CrtResource wrapper, String name)45         public ResourceInstance(CrtResource wrapper, String name) {
46             canonicalName = name;
47             this.wrapper = wrapper;
48             if (debugNativeObjects) {
49                 try {
50                     throw new RuntimeException();
51                 } catch (RuntimeException ex) {
52                     instantiation = ex;
53                 }
54             }
55         }
56 
location()57         public String location() {
58             String str = "";
59             if (debugNativeObjects) {
60                 StackTraceElement[] stack = instantiation.getStackTrace();
61 
62                 // skip ctor and acquireNativeHandle()
63                 for (int frameIdx = 2; frameIdx < stack.length; ++frameIdx) {
64                     StackTraceElement frame = stack[frameIdx];
65                     str += frame.toString() + "\n";
66                 }
67             }
68             return str;
69         }
70 
71         @Override
toString()72         public String toString() {
73             String str = canonicalName + " allocated at:\n";
74             str += location();
75             return str;
76         }
77 
getWrapper()78         public CrtResource getWrapper() { return wrapper; }
79 
setNativeHandle(long handle)80         public void setNativeHandle(long handle) { nativeHandle = handle; }
81     }
82 
83     private static final HashMap<Long, ResourceInstance> CRT_RESOURCES = new HashMap<>();
84 
85     /*
86      * Primarily intended for testing only.  Tracks the number of non-closed resources and signals
87      * whenever a zero count is reached.
88      */
89     private static boolean debugNativeObjects = System.getProperty(NATIVE_DEBUG_PROPERTY_NAME) != null;
90     private static int resourceCount = 0;
91     private static final Lock lock = new ReentrantLock();
92     private static final Condition emptyResources  = lock.newCondition();
93     private static final AtomicLong nextId = new AtomicLong(0);
94 
95     private final ArrayList<CrtResource> referencedResources = new ArrayList<>();
96     private long nativeHandle;
97     private AtomicInteger refCount = new AtomicInteger(1);
98     private long id = nextId.getAndAdd(1);
99     private Instant creationTime = Instant.now();
100     private String description;
101 
102     static {
103         /* This will cause the JNI lib to be loaded the first time a CRT is created */
CRT()104         new CRT();
105     }
106 
107     /**
108      * Default constructor
109      */
CrtResource()110     public CrtResource() {
111         if (debugNativeObjects) {
112             String canonicalName = this.getClass().getCanonicalName();
113 
114             synchronized(CrtResource.class) {
115                 CRT_RESOURCES.put(id, new ResourceInstance(this, canonicalName));
116             }
117 
118             Log.log(ResourceLogLevel, Log.LogSubject.JavaCrtResource, String.format("CrtResource of class %s(%d) created", this.getClass().getCanonicalName(), id));
119         }
120     }
121 
122     /**
123      * Marks a resource as referenced by this resource.
124      * @param resource The resource to add a reference to
125      */
addReferenceTo(CrtResource resource)126     public void addReferenceTo(CrtResource resource) {
127         resource.addRef();
128         synchronized(this) {
129             referencedResources.add(resource);
130         }
131 
132         if (debugNativeObjects) {
133             Log.log(ResourceLogLevel, Log.LogSubject.JavaCrtResource, String.format("Instance of class %s(%d) is adding a reference to instance of class %s(%d)", this.getClass().getCanonicalName(), id, resource.getClass().getCanonicalName(), resource.id));
134         }
135     }
136 
137     /**
138      * Removes a reference from this resource to another.
139      * @param resource The resource to remove a reference to
140      */
removeReferenceTo(CrtResource resource)141     public void removeReferenceTo(CrtResource resource) {
142         boolean removed = false;
143         synchronized(this) {
144             removed = referencedResources.remove(resource);
145         }
146 
147         if (debugNativeObjects) {
148             if (removed) {
149                 Log.log(ResourceLogLevel, Log.LogSubject.JavaCrtResource, String.format("Instance of class %s(%d) is removing a reference to instance of class %s(%d)", this.getClass().getCanonicalName(), id, resource.getClass().getCanonicalName(), resource.id));
150             } else {
151                 Log.log(ResourceLogLevel, Log.LogSubject.JavaCrtResource, String.format("Instance of class %s(%d) erroneously tried to remove a reference to instance of class %s(%d) that it was not referencing", this.getClass().getCanonicalName(), id, resource.getClass().getCanonicalName(), resource.id));
152             }
153         }
154 
155         if (!removed) {
156             return;
157         }
158 
159         resource.decRef();
160     }
161 
162     /**
163      * Swaps a reference from one resource to another
164      * @param oldReference resource to stop referencing
165      * @param newReference resource to start referencing
166      */
swapReferenceTo(CrtResource oldReference, CrtResource newReference)167     protected void swapReferenceTo(CrtResource oldReference, CrtResource newReference) {
168         if (oldReference != newReference) {
169             if (newReference != null) {
170                 addReferenceTo(newReference);
171             }
172             if (oldReference != null) {
173                 removeReferenceTo(oldReference);
174             }
175         }
176     }
177 
178     /**
179      * Takes ownership of a native object where the native pointer is tracked as a long.
180      * @param handle pointer to the native object being acquired
181      */
acquireNativeHandle(long handle)182     protected void acquireNativeHandle(long handle) {
183         if (!isNull()) {
184             throw new IllegalStateException("Can't acquire >1 Native Pointer");
185         }
186 
187         String canonicalName = this.getClass().getCanonicalName();
188 
189         if (handle == NULL) {
190             throw new IllegalStateException("Can't acquire NULL Pointer: " + canonicalName);
191         }
192 
193         if (debugNativeObjects) {
194             synchronized(CrtResource.class) {
195                 ResourceInstance instance = CRT_RESOURCES.get(id);
196                 if (instance != null) {
197                     instance.setNativeHandle(handle);
198                 }
199             }
200             Log.log(ResourceLogLevel, Log.LogSubject.JavaCrtResource, String.format("acquireNativeHandle - %s(%d) acquired native pointer %d", canonicalName, id, handle));
201         }
202 
203         nativeHandle = handle;
204         incrementNativeObjectCount();
205     }
206 
207     /**
208      * Begins the cleanup process associated with this native object and performs various debug-level bookkeeping operations.
209      */
release()210     private void release() {
211         if (debugNativeObjects) {
212             Log.log(ResourceLogLevel, Log.LogSubject.JavaCrtResource, String.format("Releasing class %s(%d)", this.getClass().getCanonicalName(), id));
213 
214             synchronized(CrtResource.class) {
215                 CRT_RESOURCES.remove(id);
216             }
217         }
218 
219         releaseNativeHandle();
220 
221         if (nativeHandle != 0) {
222             decrementNativeObjectCount();
223 
224             nativeHandle = 0;
225         }
226     }
227 
228     /**
229      * returns the native handle associated with this CRTResource.
230      * @return native address
231      */
getNativeHandle()232     public long getNativeHandle() {
233         return nativeHandle;
234     }
235 
236     /**
237      * Increments the reference count to this resource.
238      */
addRef()239     public void addRef() {
240         refCount.incrementAndGet();
241     }
242 
243     /**
244      * Required override method that must begin the release process of the acquired native handle
245      */
releaseNativeHandle()246     protected abstract void releaseNativeHandle();
247 
248     /**
249      * Override that determines whether a resource releases its dependencies at the same time the native handle is released or if it waits.
250      * Resources with asynchronous shutdown processes should override this with false, and establish a callback from native code that
251      * invokes releaseReferences() when the asynchronous shutdown process has completed.  See HttpClientConnectionManager for an example.
252      * @return true if this resource releases synchronously, false if this resource performs async shutdown
253      */
canReleaseReferencesImmediately()254     protected abstract boolean canReleaseReferencesImmediately();
255 
256     /**
257      * Checks if this resource's native handle is NULL.  For always-null resources this is always true.  For all other
258      * resources it means it has already been cleaned up or was not properly constructed.
259      * @return true if no native resource is bound, false otherwise
260      */
isNull()261     public boolean isNull() {
262         return (nativeHandle == NULL);
263     }
264 
265     /*
266      * An ugly and unfortunate necessity.  The CRTResource currently entangles two loosely-coupled concepts:
267      *  (1) management of a native resource
268      *  (2) referencing of other resources and the resulting implied cleanup process
269      *
270      * Some classes don't represent an actual native resource.  Instead, they just want to use
271      * the reference and cleanup framework.  See AwsIotMqttConnectionBuilder.java for example.
272      *
273      */
274 
275     @Override
close()276     public void close() {
277         decRef();
278     }
279 
280     /**
281      * Decrements the reference count to this resource.  If zero is reached, begins (and possibly completes) the resource's
282      * cleanup process.
283      */
decRef()284     public void decRef() {
285         int remainingRefs = refCount.decrementAndGet();
286 
287         if (debugNativeObjects) {
288             Log.log(ResourceLogLevel, Log.LogSubject.JavaCrtResource, String.format("Closing instance of class %s(%d) with %d remaining refs", this.getClass().getCanonicalName(), id, remainingRefs));
289         }
290 
291         if (remainingRefs != 0) {
292             return;
293         }
294 
295         release();
296 
297         if (canReleaseReferencesImmediately()) {
298             releaseReferences();
299         }
300     }
301 
302     /**
303      * Decrements the ref counts for all resources referenced by this resource.  Most resources will have this called
304      * from their close() function, but resources with asynchronous shutdown processes must have it called from a
305      * shutdown completion callback.
306      */
releaseReferences()307     protected void releaseReferences() {
308         if (debugNativeObjects) {
309             Log.log(ResourceLogLevel, Log.LogSubject.JavaCrtResource, String.format("Instance of class %s(%d) closing all referenced objects", this.getClass().getCanonicalName(), id));
310         }
311 
312         synchronized(this) {
313             for (CrtResource resource : referencedResources) {
314                 resource.decRef();
315             }
316 
317             referencedResources.clear();
318         }
319     }
320 
321     /**
322      * Sets a custom logging description for this resource
323      * @param description custom resource description
324      */
setDescription(String description)325     public void setDescription(String description) {
326         this.description = description;
327     }
328 
329     /**
330      * Gets a debug/diagnostic string describing this resource and its reference state
331      * @return resource diagnostic string
332      */
getResourceLogDescription()333     public String getResourceLogDescription() {
334         StringBuilder builder = new StringBuilder();
335         builder.append(String.format("[Id %d, Class %s, Refs %d](%s) - %s", id, getClass().getSimpleName(), refCount.get(), creationTime.toString(), description != null ? description : "<null>"));
336         synchronized(this) {
337             if (referencedResources.size() > 0) {
338                 builder.append("\n   Forward references by Id: ");
339                 for (CrtResource reference : referencedResources) {
340                     builder.append(String.format("%d ", reference.id));
341                 }
342             }
343         }
344 
345         return builder.toString();
346     }
347 
348     /**
349      * Applies a resource description consuming functor to all CRTResource objects
350      * @param fn function to apply to each resource description
351      */
collectNativeResources(Consumer<String> fn)352     public static void collectNativeResources(Consumer<String> fn) {
353         collectNativeResource((ResourceInstance resource) -> {
354             String str = String.format(" * Address: %d: %s", resource.nativeHandle,
355                     resource.toString());
356             fn.accept(str);
357         });
358     }
359 
360     /**
361      * Applies a generic diagnostic-gathering functor to all CRTResource objects
362      * @param fn function to apply to each outstanding Crt resource
363      */
collectNativeResource(Consumer<ResourceInstance> fn)364     public static void collectNativeResource(Consumer<ResourceInstance> fn) {
365         synchronized(CrtResource.class) {
366             for (Map.Entry<Long, ResourceInstance> entry : CRT_RESOURCES.entrySet()) {
367                 fn.accept(entry.getValue());
368             }
369         }
370     }
371 
372     /**
373      * Debug method to log all of the currently un-closed CRTResource objects.
374      */
logNativeResources()375     public static void logNativeResources() {
376         Log.log(ResourceLogLevel, Log.LogSubject.JavaCrtResource, "Dumping native object set:");
377         collectNativeResource((resource) -> {
378             Log.log(ResourceLogLevel, Log.LogSubject.JavaCrtResource, resource.getWrapper().getResourceLogDescription());
379         });
380     }
381 
382     /**
383      * Debug method to increment the current native object count.
384      */
incrementNativeObjectCount()385     private static void incrementNativeObjectCount() {
386         if (!debugNativeObjects) {
387             return;
388         }
389 
390         lock.lock();
391         try {
392             ++resourceCount;
393             Log.log(ResourceLogLevel, Log.LogSubject.JavaCrtResource, String.format("incrementNativeObjectCount - count = %d", resourceCount));
394         } finally {
395             lock.unlock();
396         }
397     }
398 
399     /**
400      * Debug method to decrement the current native object count.
401      */
decrementNativeObjectCount()402     private static void decrementNativeObjectCount() {
403         if (!debugNativeObjects) {
404             return;
405         }
406 
407         lock.lock();
408         try {
409             --resourceCount;
410             Log.log(ResourceLogLevel, Log.LogSubject.JavaCrtResource, String.format("decrementNativeObjectCount - count = %d", resourceCount));
411             if (resourceCount == 0) {
412                 emptyResources.signal();
413             }
414         } finally {
415             lock.unlock();
416         }
417     }
418 
419     /**
420      * Debug/test method to wait for the CRTResource count to drop to zero.  Times out with an exception after
421      * a period of waiting.
422      */
waitForNoResources()423     public static void waitForNoResources() {
424         ClientBootstrap.closeStaticDefault();
425         EventLoopGroup.closeStaticDefault();
426         HostResolver.closeStaticDefault();
427 
428         if (debugNativeObjects) {
429             lock.lock();
430 
431             try {
432                 long timeout = System.currentTimeMillis() + DEBUG_CLEANUP_WAIT_TIME_IN_SECONDS * 1000;
433                 while (resourceCount != 0 && System.currentTimeMillis() < timeout) {
434                     emptyResources.await(1, TimeUnit.SECONDS);
435                 }
436 
437                 if (resourceCount != 0) {
438                     Log.log(Log.LogLevel.Error, Log.LogSubject.JavaCrtResource, "waitForNoResources - timeOut");
439                     logNativeResources();
440                     throw new InterruptedException();
441                 }
442             } catch (InterruptedException e) {
443                 /* Cause tests to fail without having to go add checked exceptions to every instance */
444                 throw new RuntimeException("Timeout waiting for resource count to drop to zero");
445             } finally {
446                 lock.unlock();
447             }
448         }
449 
450         waitForGlobalResourceDestruction(DEBUG_CLEANUP_WAIT_TIME_IN_SECONDS);
451     }
452 
waitForGlobalResourceDestruction(int timeoutInSeconds)453     private static native void waitForGlobalResourceDestruction(int timeoutInSeconds);
454 }
455