Skip to content

Commit 37d9df2

Browse files
thomhurstclaude
andcommitted
perf: add caching for Hasher hash algorithms and Azure KeyVault clients
Fixes #1594 and #1596 - Use ThreadLocal<T> for hash algorithm instances in Hasher class since HashAlgorithm is not thread-safe but creating new instances per call is wasteful - Cache Azure KeyVault clients (SecretClient, CertificateClient, KeyClient) in ConcurrentDictionary by vault URI since Azure SDK clients are thread-safe and designed for reuse 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent ef8e766 commit 37d9df2

2 files changed

Lines changed: 42 additions & 13 deletions

File tree

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,40 @@
1+
using System.Collections.Concurrent;
12
using Azure.Core;
23
using Azure.Security.KeyVault.Certificates;
34
using Azure.Security.KeyVault.Keys;
45
using Azure.Security.KeyVault.Secrets;
56

67
namespace ModularPipelines.Azure;
78

9+
/// <summary>
10+
/// Provides access to Azure Key Vault clients with caching.
11+
/// </summary>
12+
/// <remarks>
13+
/// Azure SDK clients (SecretClient, CertificateClient, KeyClient) are thread-safe and designed
14+
/// to be long-lived and reused. This class caches clients by vault URI to avoid the overhead
15+
/// of creating new clients on every call.
16+
/// </remarks>
817
internal class AzureKeyVault : IAzureKeyVault
918
{
19+
private readonly ConcurrentDictionary<string, SecretClient> _secretClients = new();
20+
private readonly ConcurrentDictionary<string, CertificateClient> _certificateClients = new();
21+
private readonly ConcurrentDictionary<string, KeyClient> _keyClients = new();
22+
1023
public SecretClient GetSecretClient(Uri vaultUri, TokenCredential tokenCredential)
1124
{
12-
return new SecretClient(vaultUri, tokenCredential);
25+
var key = vaultUri.ToString();
26+
return _secretClients.GetOrAdd(key, _ => new SecretClient(vaultUri, tokenCredential));
1327
}
1428

1529
public CertificateClient GetCertificateClient(Uri vaultUri, TokenCredential tokenCredential)
1630
{
17-
return new CertificateClient(vaultUri, tokenCredential);
31+
var key = vaultUri.ToString();
32+
return _certificateClients.GetOrAdd(key, _ => new CertificateClient(vaultUri, tokenCredential));
1833
}
1934

2035
public KeyClient GetKeyClient(Uri vaultUri, TokenCredential tokenCredential)
2136
{
22-
return new KeyClient(vaultUri, tokenCredential);
37+
var key = vaultUri.ToString();
38+
return _keyClients.GetOrAdd(key, _ => new KeyClient(vaultUri, tokenCredential));
2339
}
2440
}

src/ModularPipelines/Context/Hasher.cs

Lines changed: 23 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,27 @@
33

44
namespace ModularPipelines.Context;
55

6+
/// <summary>
7+
/// Provides hashing operations using various algorithms.
8+
/// </summary>
9+
/// <remarks>
10+
/// HashAlgorithm instances are not thread-safe, so we use ThreadLocal to cache
11+
/// one instance per thread. This avoids the allocation overhead of creating new
12+
/// instances on every call while maintaining thread safety.
13+
/// </remarks>
614
internal class Hasher : IHasher
715
{
816
private readonly IHex _hex;
917
private readonly IBase64 _base64;
1018

19+
// ThreadLocal instances for thread-safe caching of hash algorithms
20+
// HashAlgorithm is not thread-safe, so each thread gets its own instance
21+
private static readonly ThreadLocal<SHA1> Sha1Algorithm = new(() => SHA1.Create());
22+
private static readonly ThreadLocal<SHA256> Sha256Algorithm = new(() => SHA256.Create());
23+
private static readonly ThreadLocal<SHA384> Sha384Algorithm = new(() => SHA384.Create());
24+
private static readonly ThreadLocal<SHA512> Sha512Algorithm = new(() => SHA512.Create());
25+
private static readonly ThreadLocal<MD5> Md5Algorithm = new(() => MD5.Create());
26+
1127
public Hasher(IHex hex, IBase64 base64)
1228
{
1329
_hex = hex;
@@ -16,36 +32,33 @@ public Hasher(IHex hex, IBase64 base64)
1632

1733
public string Sha1(string input, HashType hashType = HashType.Hex)
1834
{
19-
return ComputeHash(SHA1.Create(), input, hashType);
35+
return ComputeHash(Sha1Algorithm.Value!, input, hashType);
2036
}
2137

2238
public string Sha256(string input, HashType hashType = HashType.Hex)
2339
{
24-
return ComputeHash(SHA256.Create(), input, hashType);
40+
return ComputeHash(Sha256Algorithm.Value!, input, hashType);
2541
}
2642

2743
public string Sha384(string input, HashType hashType = HashType.Hex)
2844
{
29-
return ComputeHash(SHA384.Create(), input, hashType);
45+
return ComputeHash(Sha384Algorithm.Value!, input, hashType);
3046
}
3147

3248
public string Sha512(string input, HashType hashType = HashType.Hex)
3349
{
34-
return ComputeHash(SHA512.Create(), input, hashType);
50+
return ComputeHash(Sha512Algorithm.Value!, input, hashType);
3551
}
3652

3753
public string Md5(string input, HashType hashType = HashType.Hex)
3854
{
39-
return ComputeHash(MD5.Create(), input, hashType);
55+
return ComputeHash(Md5Algorithm.Value!, input, hashType);
4056
}
4157

4258
private string ComputeHash(HashAlgorithm hashAlgorithm, string input, HashType hashType)
4359
{
44-
using (hashAlgorithm)
45-
{
46-
var bytes = hashAlgorithm.ComputeHash(Encoding.UTF8.GetBytes(input));
60+
var bytes = hashAlgorithm.ComputeHash(Encoding.UTF8.GetBytes(input));
4761

48-
return hashType == HashType.Hex ? _hex.ToHex(bytes) : _base64.ToBase64String(bytes);
49-
}
62+
return hashType == HashType.Hex ? _hex.ToHex(bytes) : _base64.ToBase64String(bytes);
5063
}
5164
}

0 commit comments

Comments
 (0)