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