|
10 | 10 | import com.microsoft.azure.functions.sdktype.exceptions.SdkHydrationException; |
11 | 11 |
|
12 | 12 | import java.lang.reflect.Method; |
| 13 | +import java.util.Collections; |
| 14 | +import java.util.LinkedHashMap; |
| 15 | +import java.util.Map; |
13 | 16 | import java.util.logging.Logger; |
14 | 17 |
|
15 | 18 | /** |
|
20 | 23 | * This class implements the Template Method pattern, where the overall algorithm |
21 | 24 | * structure is defined in createInstance(), but specific steps are delegated to |
22 | 25 | * subclass implementations. |
| 26 | + * |
| 27 | + * Maintains an internal cache of BlobServiceClient objects keyed by the connection |
| 28 | + * environment variable name. Since BlobServiceClient holds the HttpPipeline (HTTP |
| 29 | + * client, retry policies, auth), caching at this level means all containers and |
| 30 | + * blobs under the same storage account share a single pipeline. Deriving |
| 31 | + * BlobContainerClient and BlobClient from a BlobServiceClient is free — just URL |
| 32 | + * construction, no new HTTP connections. |
23 | 33 | */ |
24 | 34 | public abstract class BaseBlobHydrator<T extends BlobMetaData> implements SdkTypeHydrator<T> { |
25 | 35 | protected static final Logger LOGGER = Logger.getLogger(BaseBlobHydrator.class.getName()); |
26 | 36 |
|
| 37 | + /** Max number of cached BlobServiceClient instances (keyed by connection env var). */ |
| 38 | + private static final int MAX_SERVICE_CLIENTS = 8; |
| 39 | + |
| 40 | + /** |
| 41 | + * LRU cache of BlobServiceClient objects, keyed by the connection environment |
| 42 | + * variable name (e.g. "AzureWebJobsStorage"). Typically a function app has 1-3 |
| 43 | + * storage connections, so a small cache suffices. |
| 44 | + */ |
| 45 | + private static final Map<String, Object> SERVICE_CLIENT_CACHE = |
| 46 | + Collections.synchronizedMap(new LinkedHashMap<String, Object>(4, 0.75f, true) { |
| 47 | + @Override |
| 48 | + protected boolean removeEldestEntry(Map.Entry<String, Object> eldest) { |
| 49 | + return size() > MAX_SERVICE_CLIENTS; |
| 50 | + } |
| 51 | + }); |
| 52 | + |
27 | 53 | /** |
28 | 54 | * Implements the SdkTypeHydrator interface method. Extracts the connection environment variable |
29 | 55 | * from metadata and delegates to the template method. |
@@ -70,6 +96,109 @@ private Object createInstance(T metaData, String envVar) throws Exception { |
70 | 96 | } |
71 | 97 | } |
72 | 98 |
|
| 99 | + /** |
| 100 | + * Gets or creates a cached BlobServiceClient for the given connection. |
| 101 | + * The BlobServiceClient holds the HttpPipeline and is the most expensive object |
| 102 | + * to create. All container and blob clients derived from it share the pipeline. |
| 103 | + * |
| 104 | + * @param metaData the metadata containing connection info |
| 105 | + * @return a BlobServiceClient instance (cached per connection env var) |
| 106 | + * @throws Exception if service client creation fails |
| 107 | + */ |
| 108 | + protected Object getOrCreateServiceClient(BlobMetaData metaData) throws Exception { |
| 109 | + final String cacheKey = metaData.getConnectionEnvVar(); |
| 110 | + Object cached = SERVICE_CLIENT_CACHE.get(cacheKey); |
| 111 | + if (cached != null) { |
| 112 | + LOGGER.fine("Service client cache hit for: " + cacheKey); |
| 113 | + return cached; |
| 114 | + } |
| 115 | + |
| 116 | + LOGGER.info("Service client cache miss for: " + cacheKey + ". Building new BlobServiceClient."); |
| 117 | + final Object serviceClient = buildServiceClient(metaData.getConnectionEnvVar()); |
| 118 | + SERVICE_CLIENT_CACHE.put(cacheKey, serviceClient); |
| 119 | + return serviceClient; |
| 120 | + } |
| 121 | + |
| 122 | + /** |
| 123 | + * Gets or creates a BlobContainerClient by deriving it from the cached BlobServiceClient. |
| 124 | + * This is a cheap operation — no new HTTP pipeline, just URL construction. |
| 125 | + * |
| 126 | + * @param metaData the metadata containing connection and container info |
| 127 | + * @return a BlobContainerClient instance |
| 128 | + * @throws Exception if creation fails |
| 129 | + */ |
| 130 | + protected Object getOrCreateContainerClient(BlobMetaData metaData) throws Exception { |
| 131 | + final Object serviceClient = getOrCreateServiceClient(metaData); |
| 132 | + return serviceClient.getClass() |
| 133 | + .getMethod("getBlobContainerClient", String.class) |
| 134 | + .invoke(serviceClient, metaData.getContainerName()); |
| 135 | + } |
| 136 | + |
| 137 | + /** |
| 138 | + * Builds a BlobServiceClient using reflection, handling both connection string |
| 139 | + * and managed identity scenarios. |
| 140 | + * |
| 141 | + * @param envVar the environment variable name for the connection |
| 142 | + * @return a new BlobServiceClient instance |
| 143 | + * @throws Exception if client creation fails |
| 144 | + */ |
| 145 | + private Object buildServiceClient(String envVar) throws Exception { |
| 146 | + final String maybeConnString = System.getenv(envVar); |
| 147 | + final ClassLoader cl = Thread.currentThread().getContextClassLoader(); |
| 148 | + |
| 149 | + if (maybeConnString != null && isConnectionString(maybeConnString)) { |
| 150 | + return buildServiceClientWithConnectionString(cl, maybeConnString); |
| 151 | + } else { |
| 152 | + final String accountName = System.getenv(envVar + "__accountName"); |
| 153 | + final String serviceUri = System.getenv(envVar + "__serviceUri"); |
| 154 | + final String blobServiceUri = System.getenv(envVar + "__blobServiceUri"); |
| 155 | + final String clientId = System.getenv(envVar + "__clientId"); |
| 156 | + |
| 157 | + final String endpoint = resolveEndpoint(accountName, serviceUri, blobServiceUri); |
| 158 | + final Object credential = buildManagedIdentityCredential(clientId); |
| 159 | + return buildServiceClientWithManagedIdentity(cl, endpoint, credential); |
| 160 | + } |
| 161 | + } |
| 162 | + |
| 163 | + private Object buildServiceClientWithConnectionString(ClassLoader cl, String connStr) throws Exception { |
| 164 | + final Class<?> builderClass = cl.loadClass("com.azure.storage.blob.BlobServiceClientBuilder"); |
| 165 | + final Object builder = builderClass.getDeclaredConstructor().newInstance(); |
| 166 | + |
| 167 | + builderClass.getMethod("connectionString", String.class).invoke(builder, connStr); |
| 168 | + |
| 169 | + final Object serviceClient = builderClass.getMethod("buildClient").invoke(builder); |
| 170 | + LOGGER.info("Built BlobServiceClient using connection string."); |
| 171 | + return serviceClient; |
| 172 | + } |
| 173 | + |
| 174 | + private Object buildServiceClientWithManagedIdentity(ClassLoader cl, String endpoint, Object credential) throws Exception { |
| 175 | + final Class<?> builderClass = cl.loadClass("com.azure.storage.blob.BlobServiceClientBuilder"); |
| 176 | + final Object builder = builderClass.getDeclaredConstructor().newInstance(); |
| 177 | + |
| 178 | + final Class<?> tokenCredClass = cl.loadClass("com.azure.core.credential.TokenCredential"); |
| 179 | + builderClass.getMethod("credential", tokenCredClass).invoke(builder, credential); |
| 180 | + builderClass.getMethod("endpoint", String.class).invoke(builder, endpoint); |
| 181 | + |
| 182 | + final Object serviceClient = builderClass.getMethod("buildClient").invoke(builder); |
| 183 | + LOGGER.info("Built BlobServiceClient using managed identity."); |
| 184 | + return serviceClient; |
| 185 | + } |
| 186 | + |
| 187 | + /** |
| 188 | + * Derives a BlobClient from a cached BlobContainerClient via getBlobClient(blobName). |
| 189 | + * This is essentially free — no HTTP pipeline build, just URL construction. |
| 190 | + * |
| 191 | + * @param containerClient a BlobContainerClient instance |
| 192 | + * @param blobName the blob name |
| 193 | + * @return a BlobClient pointing at the specific blob |
| 194 | + * @throws Exception if reflection fails |
| 195 | + */ |
| 196 | + protected Object deriveBlobClient(Object containerClient, String blobName) throws Exception { |
| 197 | + return containerClient.getClass() |
| 198 | + .getMethod("getBlobClient", String.class) |
| 199 | + .invoke(containerClient, blobName); |
| 200 | + } |
| 201 | + |
73 | 202 | /** |
74 | 203 | * Subclasses override to build their specific client using connection string authentication. |
75 | 204 | * |
|
0 commit comments