Skip to content

Commit d2fc03f

Browse files
committed
perf: cache BlobServiceClient per connection instead of BlobContainerClient per container
Refactors BaseBlobHydrator to cache BlobServiceClient (which holds the HTTP pipeline) keyed by connection env var name, instead of caching individual BlobContainerClient objects keyed by connection+container. This means all containers under the same storage account share a single HTTP pipeline. Before: Each unique container built its own pipeline (~100-200ms each) After: One pipeline per connection, container/blob clients derived for free Cache reduced from LRU-32 (BlobContainerClient) to LRU-8 (BlobServiceClient).
1 parent 3b6d569 commit d2fc03f

1 file changed

Lines changed: 129 additions & 0 deletions

File tree

  • azure-functions-java-sdktypes/src/main/java/com/microsoft/azure/functions/sdktype/blob

azure-functions-java-sdktypes/src/main/java/com/microsoft/azure/functions/sdktype/blob/BaseBlobHydrator.java

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@
1010
import com.microsoft.azure.functions.sdktype.exceptions.SdkHydrationException;
1111

1212
import java.lang.reflect.Method;
13+
import java.util.Collections;
14+
import java.util.LinkedHashMap;
15+
import java.util.Map;
1316
import java.util.logging.Logger;
1417

1518
/**
@@ -20,10 +23,33 @@
2023
* This class implements the Template Method pattern, where the overall algorithm
2124
* structure is defined in createInstance(), but specific steps are delegated to
2225
* 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.
2333
*/
2434
public abstract class BaseBlobHydrator<T extends BlobMetaData> implements SdkTypeHydrator<T> {
2535
protected static final Logger LOGGER = Logger.getLogger(BaseBlobHydrator.class.getName());
2636

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+
2753
/**
2854
* Implements the SdkTypeHydrator interface method. Extracts the connection environment variable
2955
* from metadata and delegates to the template method.
@@ -70,6 +96,109 @@ private Object createInstance(T metaData, String envVar) throws Exception {
7096
}
7197
}
7298

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+
73202
/**
74203
* Subclasses override to build their specific client using connection string authentication.
75204
*

0 commit comments

Comments
 (0)