Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Make EntityCounter create tolerant of existing #956

Merged
merged 1 commit into from
Feb 12, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -43,12 +43,8 @@ public ApiAssetRepositoryTests(DlcsDatabaseFixture dbFixture)
);
// We want this turned on to match live behaviour
dbContext.ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.TrackAll;
var config = new ConfigurationBuilder()
.AddInMemoryCollection(new[]
{ new KeyValuePair<string, string>("ConnectionStrings:PostgreSQLConnection", dbFixture.ConnectionString) })
.Build();

var entityCounterRepo = new EntityCounterRepository(dbContext);

var entityCounterRepo = new EntityCounterRepository(dbContext, new NullLogger<EntityCounterRepository>());

var assetRepositoryCachingHelper = new AssetCachingHelper(
new MockCachingService(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ public async Task<CreateApiKeyResult> Handle(SetupApplication request, Cancellat
await CreateDefaultStoragePolicy(cancellationToken);
var updateCount = await dbContext.SaveChangesAsync(cancellationToken);

await entityCounterRepository.Create(adminCustomer.Id, KnownEntityCounters.CustomerSpaces, adminCustomer.Id.ToString());
await entityCounterRepository.TryCreate(adminCustomer.Id, KnownEntityCounters.CustomerSpaces, adminCustomer.Id.ToString());

return updateCount == 2
? CreateApiKeyResult.Success(apiKey, apiSecret)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ public async Task<ModifyEntityResult<CustomerEntity>> Handle(CreateCustomer requ
var newCustomerId = customer.Id;

// create an entity counter for space IDs
await entityCounterRepository.Create(newCustomerId, KnownEntityCounters.CustomerSpaces,
await entityCounterRepository.TryCreate(newCustomerId, KnownEntityCounters.CustomerSpaces,
newCustomerId.ToString());

await CreateAuthServices(cancellationToken, newCustomerId);
Expand Down
3 changes: 2 additions & 1 deletion src/protagonist/DLCS.Model/IEntityCounterRepository.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@ public interface IEntityCounterRepository
/// <summary>
/// Create a new EntityCounter record with specified value.
/// </summary>
Task Create(int customer, string entityType, string scope, long initialValue = 1);
/// <returns>True if EntityCounter created, else false</returns>
Task<bool> TryCreate(int customer, string entityType, string scope, long initialValue = 1);

/// <summary>
/// Removes an EntityCounter from the database
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
using System;
using System.Linq;
using System.Linq;
using DLCS.Repository.Entities;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging.Abstractions;
using Test.Helpers.Integration;

namespace DLCS.Repository.Tests.Entities;
Expand All @@ -17,47 +17,49 @@ public EntityCounterRepositoryTests(DlcsDatabaseFixture dbFixture)
{
dbContext = dbFixture.DbContext;

sut = new EntityCounterRepository(dbContext);
sut = new EntityCounterRepository(dbContext, new NullLogger<EntityCounterRepository>());

dbFixture.CleanUp();
}

[Fact]
public async Task Create_AddsEntityCounter()
public async Task TryCreate_AddsEntityCounter()
{
// Arrange
const string scope = nameof(Create_AddsEntityCounter);
const string scope = nameof(TryCreate_AddsEntityCounter);
var expected = new EntityCounter
{
Customer = 1, Next = 1, Scope = scope, Type = $"{scope}_type"
};

// Act
await sut.Create(1, $"{scope}_type", scope);
var result = await sut.TryCreate(1, $"{scope}_type", scope);

// Assert
result.Should().BeTrue("Counter created");
var saved = dbContext.EntityCounters.Single(ec => ec.Scope == scope);
saved.Should().BeEquivalentTo(expected);
}

[Fact]
public async Task Create_Throws_IfRecordAlreadyExists_SameTypeScopeCustomer()
public async Task TryCreate_NoOp_ReturnsFalse_IfRecordAlreadyExists_SameTypeScopeCustomer()
{
// NOTE - this tests behaviour rather than the behaviour being correct

// Arrange
const string scope = nameof(Create_Throws_IfRecordAlreadyExists_SameTypeScopeCustomer);
dbContext.EntityCounters.Add(new EntityCounter
const string scope = nameof(TryCreate_NoOp_ReturnsFalse_IfRecordAlreadyExists_SameTypeScopeCustomer);
var entityCounter = new EntityCounter
{
Customer = 1, Next = 9999, Scope = scope, Type = $"{scope}_type"
});
};
dbContext.EntityCounters.Add(entityCounter);
await dbContext.SaveChangesAsync();

// Act
Func<Task> action = () => sut.Create(1, $"{scope}_type", scope);
var result = await sut.TryCreate(1, $"{scope}_type", scope);

// Assert
await action.Should().ThrowAsync<InvalidOperationException>();
result.Should().BeFalse("Counter already exists");
var dbCounter = dbContext.EntityCounters.Single(ec => ec.Scope == scope);
dbCounter.Should().BeEquivalentTo(entityCounter);
}

[Fact]
Expand Down
Original file line number Diff line number Diff line change
@@ -1,31 +1,32 @@
using System.Threading.Tasks;
using DLCS.Model;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;

namespace DLCS.Repository.Entities;

public class EntityCounterRepository : IDapperContextRepository, IEntityCounterRepository
{
private readonly ILogger<EntityCounterRepository> logger;
public DlcsContext DlcsContext { get; }

public EntityCounterRepository(DlcsContext dlcsContext)
public EntityCounterRepository(DlcsContext dlcsContext, ILogger<EntityCounterRepository> logger)
{
this.logger = logger;
DlcsContext = dlcsContext;
}

public async Task Create(int customer, string entityType, string scope, long initialValue = 1)
public async Task<bool> TryCreate(int customer, string entityType, string scope, long initialValue = 1)
{
var ec = new EntityCounter
{
Customer = customer,
Type = entityType,
Scope = scope,
Next = initialValue
};
await DlcsContext.EntityCounters.AddAsync(ec);
await DlcsContext.SaveChangesAsync();
var exists = await Exists(customer, entityType, scope);
if (exists) return false;

logger.LogInformation("Creating entityCounter {EntityType}:{Scope} for Customer {Customer}", entityType,
scope, customer);
await CreateEntity(customer, entityType, scope, initialValue);
return true;
}

public async Task Remove(int customer, string entityType, string scope, long nextValue)
{
var ec = new EntityCounter
Expand Down Expand Up @@ -59,19 +60,23 @@ private Task<bool> Exists(int customer, string entityType, string scope) =>
DlcsContext.EntityCounters.AnyAsync(ec =>
ec.Customer == customer && ec.Type == entityType && ec.Scope == scope);

private async Task EnsureCounter(int customer, string entityType, string scope, long initialValue)
private async Task<long> LongUpdate(string sql, int customer, string entityType, string scope, long initialValue = 1)
{
var exists = await Exists(customer, entityType, scope);
if (!exists)
{
await Create(customer, entityType, scope, initialValue);
}
await TryCreate(customer, entityType, scope, initialValue);
return await this.QuerySingleAsync<long>(sql, new { customer, entityType, scope });
}

private async Task<long> LongUpdate(string sql, int customer, string entityType, string scope, long initialValue = 1)
private async Task CreateEntity(int customer, string entityType, string scope, long initialValue = 1)
{
await EnsureCounter(customer, entityType, scope, initialValue);
return await this.QuerySingleAsync<long>(sql, new { customer, entityType, scope });
var ec = new EntityCounter
{
Customer = customer,
Type = entityType,
Scope = scope,
Next = initialValue
};
await DlcsContext.EntityCounters.AddAsync(ec);
await DlcsContext.SaveChangesAsync();
}

private const string GetNextSql = @"
Expand Down
65 changes: 28 additions & 37 deletions src/protagonist/DLCS.Repository/Spaces/SpaceRepository.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ public class SpaceRepository : ISpaceRepository
private readonly IEntityCounterRepository entityCounterRepository;
private readonly CacheSettings cacheSettings;
private readonly IAppCache appCache;
private ILogger<SpaceRepository> logger;
private readonly ILogger<SpaceRepository> logger;

private const int DefaultMaxUnauthorised = -1;

Expand Down Expand Up @@ -88,7 +88,7 @@ public async Task<Space> CreateSpace(int customer, string name, string? imageBuc
};

await dlcsContext.Spaces.AddAsync(space, cancellationToken);
await entityCounterRepository.Create(customer, KnownEntityCounters.SpaceImages, space.Id.ToString());
await entityCounterRepository.TryCreate(customer, KnownEntityCounters.SpaceImages, space.Id.ToString());
await dlcsContext.SaveChangesAsync(cancellationToken);
return space;
}
Expand All @@ -108,7 +108,6 @@ private async Task<int> GetIdForNewSpace(int requestCustomer)

return newModelId;
}


private async Task<Space?> GetSpaceInternal(int customerId, int spaceId,
CancellationToken cancellationToken, string? name = null,
Expand Down Expand Up @@ -261,59 +260,51 @@ public async Task<Space> PutSpace(
};

await dlcsContext.Spaces.AddAsync(space, cancellationToken);
await entityCounterRepository.Create(customerId, KnownEntityCounters.SpaceImages, space.Id.ToString());
await entityCounterRepository.TryCreate(customerId, KnownEntityCounters.SpaceImages, space.Id.ToString());
}

await dlcsContext.SaveChangesAsync(cancellationToken);

var retrievedSpace = await GetSpaceInternal(customerId, spaceId, cancellationToken, noCache:true);
return retrievedSpace;
}

public async Task<ResultMessage<DeleteResult>> DeleteSpace(int customerId, int spaceId, CancellationToken cancellationToken)
{
var deleteResult = DeleteResult.NotFound;
var message = string.Empty;

public async Task<ResultMessage<DeleteResult>> DeleteSpace(int customerId, int spaceId,
CancellationToken cancellationToken)
{
try
{
var space = await dlcsContext.Spaces.SingleOrDefaultAsync(s =>
s.Customer == customerId && s.Id == spaceId, cancellationToken: cancellationToken);

if (space != null)
if (space == null)
{
var images = await dlcsContext.Images.AsNoTracking().FirstOrDefaultAsync(i =>
i.Customer == customerId && i.Space == spaceId, cancellationToken);

if (images == null)
{
dlcsContext.Spaces.Remove(space);
await dlcsContext.SaveChangesAsync(cancellationToken);

await entityCounterRepository.Decrement(customerId, KnownEntityCounters.CustomerSpaces,
customerId.ToString());
await entityCounterRepository.Remove(customerId, KnownEntityCounters.SpaceImages,
space.Id.ToString(), 1);
deleteResult = DeleteResult.Deleted;
}
else
{
deleteResult = DeleteResult.Conflict;
message = "Cannot delete a space with images";
}
return new ResultMessage<DeleteResult>($"Space {spaceId} not found", DeleteResult.NotFound);
}

var hasImages = await dlcsContext.Images.AnyAsync(i =>
i.Customer == customerId && i.Space == spaceId, cancellationToken);

if (hasImages)
{
return new ResultMessage<DeleteResult>("Cannot delete a space with images", DeleteResult.Conflict);
}

dlcsContext.Spaces.Remove(space);
await dlcsContext.SaveChangesAsync(cancellationToken);

await entityCounterRepository.Decrement(customerId, KnownEntityCounters.CustomerSpaces,
customerId.ToString());
await entityCounterRepository.Remove(customerId, KnownEntityCounters.SpaceImages,
space.Id.ToString(), 1);
return new ResultMessage<DeleteResult>(string.Empty, DeleteResult.Deleted);
}
catch (Exception exception)
{
message = "Error deleting space";
deleteResult = DeleteResult.Error;
logger.LogError(exception, "Failed to delete space");
logger.LogError(exception, "Failed to delete space {Customer}/{Space}", customerId, spaceId);
return new ResultMessage<DeleteResult>("Error deleting space", DeleteResult.Error);
}

var resultMessage = new ResultMessage<DeleteResult>(message, deleteResult);

return resultMessage;
}

private static long GetApproximateImages(long entityCounterNext) => Math.Max(entityCounterNext - 1, 0);
}
Loading