22using System . IO . Compression ;
33using System . Threading ;
44using System . Threading . Tasks ;
5+ using ApiService . OneFuzzLib . Orm ;
56using Azure ;
67using Azure . Core ;
8+ using Azure . ResourceManager . Storage ;
79using Azure . Storage . Blobs ;
810using Azure . Storage . Blobs . Models ;
911using Azure . Storage . Blobs . Specialized ;
1012using Azure . Storage . Sas ;
13+ using Microsoft . Extensions . Caching . Memory ;
1114using Microsoft . Extensions . Logging ;
1215namespace Microsoft . OneFuzz . Service ;
1316
@@ -20,6 +23,7 @@ public interface IContainers {
2023 public Async . Task < BlobContainerClient ? > GetOrCreateContainerClient ( Container container , StorageType storageType , IDictionary < string , string > ? metadata ) ;
2124
2225 public Async . Task < BlobContainerClient ? > FindContainer ( Container container , StorageType storageType ) ;
26+ public Async . Task < bool > DeleteContainerIfExists ( Container container , StorageType storageType ) ;
2327
2428 public Async . Task < Uri ? > GetFileSasUrl ( Container container , string name , StorageType storageType , BlobSasPermissions permissions , TimeSpan ? duration = null ) ;
2529 public Async . Task SaveBlob ( Container container , string name , string data , StorageType storageType , DateOnly ? expiresOn = null ) ;
@@ -40,19 +44,26 @@ public interface IContainers {
4044 public Async . Task DeleteAllExpiredBlobs ( ) ;
4145}
4246
43- public class Containers : IContainers {
47+ public class Containers : Orm < ContainerInformation > , IContainers {
4448 private readonly ILogger _log ;
4549 private readonly IStorage _storage ;
4650 private readonly IServiceConfig _config ;
47- private readonly IOnefuzzContext _context ;
51+ private readonly IMemoryCache _cache ;
4852
4953 static readonly TimeSpan CONTAINER_SAS_DEFAULT_DURATION = TimeSpan . FromDays ( 30 ) ;
54+ static readonly TimeSpan CONTAINER_INFO_EXPIRATION_TIME = TimeSpan . FromMinutes ( 10 ) ;
55+
56+ public Containers (
57+ ILogger < Containers > log ,
58+ IStorage storage ,
59+ IServiceConfig config ,
60+ IOnefuzzContext context ,
61+ IMemoryCache cache ) : base ( log , context ) {
5062
51- public Containers ( ILogger < Containers > log , IStorage storage , IServiceConfig config , IOnefuzzContext context ) {
5263 _log = log ;
5364 _storage = storage ;
5465 _config = config ;
55- _context = context ;
66+ _cache = cache ;
5667
5768 _getInstanceId = new Lazy < Async . Task < Guid > > ( async ( ) => {
5869 var ( data , tags ) = await GetBlob ( WellKnownContainers . BaseConfig , "instance_id" , StorageType . Config ) ;
@@ -74,7 +85,6 @@ public Containers(ILogger<Containers> log, IStorage storage, IServiceConfig conf
7485
7586 public async Async . Task < ( BinaryData ? data , IDictionary < string , string > ? tags ) > GetBlob ( Container container , string name , StorageType storageType ) {
7687 var client = await FindContainer ( container , storageType ) ;
77-
7888 if ( client == null ) {
7989 return ( null , null ) ;
8090 }
@@ -125,32 +135,105 @@ private static readonly BlobContainerSasPermissions _containerCreatePermissions
125135 return null ;
126136 }
127137
128- return cc ;
129- }
138+ // record the fact that we created a new container
139+ var resourceId = BlobContainerResource . CreateResourceIdentifier (
140+ account . SubscriptionId ,
141+ account . ResourceGroupName ,
142+ account . Name ,
143+ containerName ) ;
130144
145+ _ = await SetContainerInformation ( container , storageType , resourceId ) ;
131146
132- public async Async . Task < BlobContainerClient ? > FindContainer ( Container container , StorageType storageType ) {
133- // # check secondary accounts first by searching in reverse.
134- // #
135- // # By implementation, the primary account is specified first, followed by
136- // # any secondary accounts.
137- // #
138- // # Secondary accounts, if they exist, are preferred for containers and have
139- // # increased IOP rates, this should be a slight optimization
147+ return cc ;
148+ }
140149
150+ private async Task < ResourceIdentifier ? > FindContainerInAccounts ( Container container , StorageType storageType ) {
141151 var containerName = _config . OneFuzzStoragePrefix + container ;
142-
152+ // Check secondary accounts first by searching in reverse.
153+ //
154+ // By implementation, the primary account is specified first, followed by
155+ // any secondary accounts.
156+ //
157+ // Secondary accounts, if they exist, are preferred for containers and have
158+ // increased IOP rates, this should be a slight optimization
143159 foreach ( var account in _storage . GetAccounts ( storageType ) . Reverse ( ) ) {
144160 var accountClient = await _storage . GetBlobServiceClientForAccount ( account ) ;
145161 var containerClient = accountClient . GetBlobContainerClient ( containerName ) ;
146162 if ( await containerClient . ExistsAsync ( ) ) {
147- return containerClient ;
163+ return BlobContainerResource . CreateResourceIdentifier (
164+ account . SubscriptionId ,
165+ account . ResourceGroupName ,
166+ account . Name ,
167+ containerName ) ;
148168 }
149169 }
150170
151171 return null ;
152172 }
153173
174+ private sealed record ContainerKey ( StorageType storageType , Container container ) ;
175+ private async Task < ContainerInformation > SetContainerInformation ( Container container , StorageType storageType , ResourceIdentifier resourceId ) {
176+ var containerInfo = new ContainerInformation ( storageType , container , resourceId . ToString ( ) ) ;
177+ _ = await Replace ( containerInfo ) ;
178+ _ = _cache . Set ( new ContainerKey ( storageType , container ) , containerInfo , CONTAINER_INFO_EXPIRATION_TIME ) ;
179+ return containerInfo ;
180+ }
181+
182+ private async Task < bool > DeleteContainerInformation ( Container container , StorageType storageType ) {
183+ var result = await DeleteIfExists ( storageType . ToString ( ) , container . ToString ( ) ) ;
184+ _cache . Remove ( new ContainerKey ( storageType , container ) ) ;
185+ return result . IsOk && result . OkV ;
186+ }
187+
188+ private async Task < ContainerInformation ? > LoadContainerInformation ( Container container , StorageType storageType ) {
189+ // first, try cache
190+ var info = _cache . Get < ContainerInformation > ( new ContainerKey ( storageType , container ) ) ;
191+ if ( info is not null ) {
192+ return info ;
193+ }
194+
195+ // next try the table
196+ var result = await QueryAsync ( Query . SingleEntity ( storageType . ToString ( ) , container . ToString ( ) ) ) . FirstOrDefaultAsync ( ) ;
197+ if ( result is not null ) {
198+ _ = _cache . Set ( new ContainerKey ( storageType , container ) , result , CONTAINER_INFO_EXPIRATION_TIME ) ;
199+ return result ;
200+ }
201+
202+ // we don't have metadata in the table about this account yet, find it:
203+ var resourceId = await FindContainerInAccounts ( container , storageType ) ;
204+ if ( resourceId is null ) {
205+ // never negatively-cache container info, so containers created by other instances
206+ // can be found instantly
207+ return null ;
208+ }
209+
210+ // we found the container, insert it into the table (and cache) so we find it next time:
211+ return await SetContainerInformation ( container , storageType , resourceId ) ;
212+ }
213+
214+ public async Async . Task < BlobContainerClient ? > FindContainer ( Container container , StorageType storageType ) {
215+ var containerInfo = await LoadContainerInformation ( container , storageType ) ;
216+ if ( containerInfo is null ) {
217+ return null ;
218+ }
219+
220+ return await _storage . GetBlobContainerClientForContainerResource ( new ResourceIdentifier ( containerInfo . ResourceId ) ) ;
221+ }
222+
223+ public async Async . Task < bool > DeleteContainerIfExists ( Container container , StorageType storageType ) {
224+ var client = await FindContainer ( container , storageType ) ;
225+ if ( client is null ) {
226+ // container doesn't exist
227+ return false ;
228+ }
229+
230+ var ( _, result ) = await (
231+ DeleteContainerInformation ( container , storageType ) ,
232+ client . DeleteIfExistsAsync ( ) ) ;
233+
234+ return result ;
235+ }
236+
154237 public async Async . Task < Uri ? > GetFileSasUrl ( Container container , string name , StorageType storageType , BlobSasPermissions permissions , TimeSpan ? duration = null ) {
155238 var client = await FindContainer ( container , storageType ) ;
156239 if ( client is null ) {
0 commit comments