Skip to content
This repository was archived by the owner on Nov 1, 2023. It is now read-only.

Commit 51591b7

Browse files
authored
Cache FindContainer (#3304)
Closes #3156. From now on, we will store additional information about containers in a table (`ContainerInformation`). This is populated when a container is created, or lazily when looking for a container we have not yet stored in the table. Reads from this table are also cached in memory for a short period of time (currently 10 minutes). The table is currently used to speed up lookups for containers across storage accounts; we round-robin creation of containers across multiple storage accounts to spread load, so when we read from a container we need to find what storage account it lives in. The `ContainerInformation` table could also be used in future to store information about access control (see, e.g. #1419).
1 parent c44bd03 commit 51591b7

File tree

12 files changed

+291
-91
lines changed

12 files changed

+291
-91
lines changed

src/ApiService/ApiService/Functions/Containers.cs

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ private async Async.Task<HttpResponseData> Get(HttpRequestData req) {
4141
req,
4242
Error.Create(
4343
ErrorCode.INVALID_REQUEST,
44-
"invalid container"),
44+
$"invalid container '{get.Name}'"),
4545
context: get.Name.String);
4646
}
4747

@@ -75,13 +75,7 @@ private async Async.Task<HttpResponseData> Delete(HttpRequestData req) {
7575

7676
var delete = request.OkV;
7777
_logger.LogInformation("deleting {ContainerName}", delete.Name);
78-
var container = await _context.Containers.FindContainer(delete.Name, StorageType.Corpus);
79-
80-
var deleted = false;
81-
if (container is not null) {
82-
deleted = await container.DeleteIfExistsAsync();
83-
}
84-
78+
var deleted = await _context.Containers.DeleteContainerIfExists(delete.Name, StorageType.Corpus);
8579
return await RequestHandling.Ok(req, deleted);
8680
}
8781

src/ApiService/ApiService/OneFuzzTypes/Model.cs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -173,7 +173,7 @@ public static Error Create(ErrorCode code, params string[] errors)
173173
=> new(code, errors.ToList());
174174

175175
public sealed override string ToString() {
176-
var errorsString = Errors != null ? string.Concat("; ", Errors) : string.Empty;
176+
var errorsString = Errors != null ? string.Join("; ", Errors) : string.Empty;
177177
return $"Error {{ Code = {Code}, Errors = {errorsString} }}";
178178
}
179179
};
@@ -767,6 +767,11 @@ public record ClientCredentials
767767
string ClientSecret
768768
);
769769

770+
public record ContainerInformation(
771+
[PartitionKey] StorageType Type,
772+
[RowKey] Container Name,
773+
string ResourceId // full ARM resource ID for the container
774+
) : EntityBase;
770775

771776
public record AgentConfig(
772777
ClientCredentials? ClientCredentials,

src/ApiService/ApiService/OneFuzzTypes/ReturnTypes.cs

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,16 @@
22

33
namespace Microsoft.OneFuzz.Service {
44

5+
public static class Result {
6+
public readonly record struct OkPlaceholder();
7+
public readonly record struct OkPlaceholder<T>(T Value);
8+
public readonly record struct ErrPlaceholder<T>(T Value);
9+
10+
public static OkPlaceholder Ok() => new();
11+
public static OkPlaceholder<T> Ok<T>(T value) => new(value);
12+
public static ErrPlaceholder<T> Error<T>(T value) => new(value);
13+
}
14+
515
public readonly struct ResultVoid<T_Error> {
616
public static ResultVoid<T_Error> Ok() => new();
717
public static ResultVoid<T_Error> Error(T_Error err) => new(err);
@@ -13,15 +23,16 @@ public readonly struct ResultVoid<T_Error> {
1323
public bool IsOk { get; }
1424

1525
public T_Error? ErrorV { get; }
16-
}
1726

27+
public static implicit operator ResultVoid<T_Error>(Result.OkPlaceholder _ok) => Ok();
28+
public static implicit operator ResultVoid<T_Error>(Result.ErrPlaceholder<T_Error> err) => Error(err.Value);
29+
}
1830

1931
public readonly struct Result<T_Ok, T_Error> {
2032
public static Result<T_Ok, T_Error> Ok(T_Ok ok) => new(ok);
2133
public static Result<T_Ok, T_Error> Error(T_Error err) => new(err);
2234

2335
private Result(T_Ok ok) => (OkV, ErrorV, IsOk) = (ok, default, true);
24-
2536
private Result(T_Error error) => (ErrorV, OkV, IsOk) = (error, default, false);
2637

2738
[MemberNotNullWhen(returnValue: true, member: nameof(OkV))]
@@ -31,6 +42,9 @@ public readonly struct Result<T_Ok, T_Error> {
3142
public T_Error? ErrorV { get; }
3243

3344
public T_Ok? OkV { get; }
45+
46+
public static implicit operator Result<T_Ok, T_Error>(Result.OkPlaceholder<T_Ok> ok) => Ok(ok.Value);
47+
public static implicit operator Result<T_Ok, T_Error>(Result.ErrPlaceholder<T_Error> err) => Error(err.Value);
3448
}
3549

3650
public static class OneFuzzResult {
@@ -61,6 +75,8 @@ public readonly struct OneFuzzResult<T_Ok> {
6175

6276
// Allow simple conversion of Errors to Results.
6377
public static implicit operator OneFuzzResult<T_Ok>(Error err) => new(err);
78+
public static implicit operator OneFuzzResult<T_Ok>(Result.OkPlaceholder<T_Ok> ok) => Ok(ok.Value);
79+
public static implicit operator OneFuzzResult<T_Ok>(Result.ErrPlaceholder<Error> err) => Error(err.Value);
6480
}
6581

6682
public readonly struct OneFuzzResultVoid {
@@ -83,5 +99,7 @@ public readonly struct OneFuzzResultVoid {
8399

84100
// Allow simple conversion of Errors to Results.
85101
public static implicit operator OneFuzzResultVoid(Error err) => new(err);
102+
public static implicit operator OneFuzzResultVoid(Result.OkPlaceholder _ok) => Ok;
103+
public static implicit operator OneFuzzResultVoid(Result.ErrPlaceholder<Error> err) => Error(err.Value);
86104
}
87105
}

src/ApiService/ApiService/onefuzzlib/Containers.cs

Lines changed: 100 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,15 @@
22
using System.IO.Compression;
33
using System.Threading;
44
using System.Threading.Tasks;
5+
using ApiService.OneFuzzLib.Orm;
56
using Azure;
67
using Azure.Core;
8+
using Azure.ResourceManager.Storage;
79
using Azure.Storage.Blobs;
810
using Azure.Storage.Blobs.Models;
911
using Azure.Storage.Blobs.Specialized;
1012
using Azure.Storage.Sas;
13+
using Microsoft.Extensions.Caching.Memory;
1114
using Microsoft.Extensions.Logging;
1215
namespace 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) {

src/ApiService/ApiService/onefuzzlib/Storage.cs

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
using System.Text.Json;
1+
using System.Diagnostics;
2+
using System.Text.Json;
23
using System.Threading.Tasks;
34
using Azure.Core;
45
using Azure.Data.Tables;
@@ -47,6 +48,21 @@ public Task<BlobServiceClient> GetBlobServiceClientForAccount(ResourceIdentifier
4748

4849
public Task<BlobServiceClient> GetBlobServiceClientForAccountName(string accountName);
4950

51+
public async Task<BlobContainerClient> GetBlobContainerClientForContainerResource(ResourceIdentifier containerResourceId) {
52+
if (containerResourceId.ResourceType != BlobContainerResource.ResourceType) {
53+
throw new ArgumentException("must be a container resource ID", nameof(containerResourceId));
54+
}
55+
56+
// parent of container resource = blob services resource
57+
// parent of blob services resource = storage account resource
58+
var accountId = containerResourceId.Parent?.Parent ?? throw new ArgumentException("resource ID must have parent", nameof(containerResourceId));
59+
60+
Debug.Assert(accountId.ResourceType == StorageAccountResource.ResourceType);
61+
62+
var accountClient = await GetBlobServiceClientForAccount(accountId);
63+
return accountClient.GetBlobContainerClient(containerResourceId.Name);
64+
}
65+
5066
public Uri GenerateBlobContainerSasUri(
5167
BlobContainerSasPermissions permissions,
5268
BlobContainerClient containerClient,

src/ApiService/ApiService/onefuzzlib/orm/EntityConverter.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -257,6 +257,8 @@ public async Async.Task<TableEntity> ToTableEntity<T>(T typedEntity) where T : E
257257
return int.Parse(stringValue);
258258
} else if (ef.type == typeof(long)) {
259259
return long.Parse(stringValue);
260+
} else if (ef.type.IsEnum) {
261+
return Enum.Parse(ef.type, stringValue);
260262
} else if (ef.type.IsClass) {
261263
try {
262264
if (ef.type.GetMethod("Parse", BindingFlags.Static | BindingFlags.Public) is MethodInfo mi) {
@@ -275,7 +277,6 @@ public async Async.Task<TableEntity> ToTableEntity<T>(T typedEntity) where T : E
275277
var fieldName = ef.columnName;
276278
var obj = entity[fieldName];
277279
if (obj == null) {
278-
279280
if (ef.parameterInfo.HasDefaultValue) {
280281
return ef.parameterInfo.DefaultValue;
281282
}

0 commit comments

Comments
 (0)