Skip to content
Closed
Show file tree
Hide file tree
Changes from 3 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 @@ -5,5 +5,7 @@

[assembly: CosmosDbConfiguredCondition]

// Waiting on Task causes deadlocks when run in parallel
[assembly: CollectionBehavior(DisableTestParallelization = true)]
// Emulator could experience performance degradation with more than 10 concurrent containers,
// some tests might create multiple containers
// https://learn.microsoft.com/en-us/azure/cosmos-db/emulator#differences-between-the-emulator-and-cloud-service
[assembly: CollectionBehavior(MaxParallelThreads = 4)]
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ namespace Microsoft.EntityFrameworkCore.TestUtilities;

public class CosmosTestStore : TestStore
{
private static List<string>? _createdDatabases = [];
private static readonly SemaphoreSlim _collectionsSemaphore = new(1);
private readonly TestStoreContext _storeContext;
private readonly string? _dataFilePath;
private readonly Action<CosmosDbContextOptionsBuilder> _configureCosmos;
Expand Down Expand Up @@ -75,9 +77,19 @@ private CosmosTestStore(
}

private static string CreateName(string name)
=> TestEnvironment.IsEmulator || name == "Northwind" || name == "Northwind2" || name == "Northwind3"
? name
: name + _runId;
{
if (TestEnvironment.IsEmulator)
{
return "EF-" + Guid.NewGuid().ToString();
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@AndriySvyryd do you think we can get rid of the GUID-named containers? I'm not sure what emulator limitations make this necessary and why we need to delete etc.

Copy link
Copy Markdown
Contributor Author

@JoasE JoasE Feb 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Its documented that the emulator will show performance degradation with >10 containers in this statement in the docs

Which I think is why its deleting the containers after every test.

Because different test classes might use the same test store, using a guid was a quick fix to allow parallel execution (as we otherwise will start deleting something while it's still being used.) There might be a better way to do this, or the performance of >10 containers isn't that bad (or is more about >10 active containers). I'm still looking into that.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah thanks, I missed that note. I'm guessing that's also a good reason to not parallelize (or at least to keep the degree of parallelization down)...

But I'm still not sure why it's useful to include a random GUID inside the container name. That feels more like a practice for when a shared cloud database is used potentially in parallel, to ensure you don't have conflicts - not relevant for the emulator.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@roji
The GUID guarantees that parallel execution becomes safe by ensuring every test gets its own isolated container.
Since different test classes might use the same fixture and therefore the same test store name, conflicts will occur with parallel test execution. The tests sharing fixtures are (usually, or as far as I saw until now) read only tests, but since we are deleting the containers one test might finish and delete the container while another is still using it. Using a GUID here is a quick fix, where another option would be to group these tests and create a collection fixture, tying the container lifetime to the collection fixture, but that might require significant changes in the setup with the specifications test or throughout the entire cosmos tests project. Which I am not sure is worth the effort at this moment as I am still trying to determine whether running tests in parallel is actually feasible with the emulator.

It currently seems to be stable locally with no limit on the parallelism (which would mean processor count, which is 16 for my laptop), and no significant performance increment compared to a limit of 3 threads. With by stable I mean running the complete test suite 15 times with no errors.

If I don't delete the shared containers the emulator will stop responding to create collection requests at some point, around 20 databases with each 1-3 containers.

I haven't tested this against a real cosmos db tho, and enabling multi threading is not conditional, so I still have to do that. Maybe I could handle the parallelism myself with semaphores in the test store completely, but it might be kinda funky, also affecting test run times for tests that don't use a fixture but call InitializeAsync in the test.

I just want to mention that this is still an experiment, and I’m not sure yet whether it will succeed or if I’ll even complete it. Please don’t feel like you need to limit your questions though, I really appreciate the input and enjoy discussing it. I just don’t want you to feel like you have to spend time on it, unless you are ok with that!

}

if (name == "Northwind" || name == "Northwind2" || name == "Northwind3")
{
return name;
}

return name + _runId;
}

public string ConnectionUri { get; }
public string AuthToken { get; }
Expand Down Expand Up @@ -108,6 +120,7 @@ public static async ValueTask<bool> IsConnectionAvailableAsync()
{
_connectionSemaphore.Release();
}

}

return _connectionAvailable.Value;
Expand All @@ -120,6 +133,25 @@ private static async Task<bool> TryConnectAsync()
{
testStore = await CreateInitializedAsync("NonExistent").ConfigureAwait(false);

if (TestEnvironment.IsEmulator)
{
var client = testStore.CreateDefaultContext().Database.GetCosmosClient();
using var iterator = client.GetDatabaseQueryIterator<DatabaseProperties>();

while (iterator.HasMoreResults)
{
var response = await iterator.ReadNextAsync();

foreach (var db in response.Where(x => x.Id.StartsWith("EF-") && !_createdDatabases!.Contains(x.Id)))
{
await client.GetDatabase(db.Id).DeleteAsync();
}
}

_createdDatabases = null;
}


return true;
}
catch (AggregateException aggregate)
Expand Down Expand Up @@ -179,13 +211,33 @@ protected override async Task InitializeAsync(Func<DbContext> createContext, Fun
}
}

private async Task<bool> ContextEnsureCreated(DbContext context)
{
var aquired = false;
if (TestEnvironment.IsEmulator)
{
aquired = await _collectionsSemaphore.WaitAsync(60_000);
}
try
{
return await context.Database.EnsureCreatedAsync().ConfigureAwait(false);
}
finally
{
if (aquired)
{
_collectionsSemaphore.Release();
}
}
}

private async Task CreateFromFile(DbContext context)
{
if (await EnsureCreatedAsync(context).ConfigureAwait(false))
{
if (!TestEnvironment.UseTokenCredential)
{
await context.Database.EnsureCreatedAsync().ConfigureAwait(false);
await ContextEnsureCreated(context).ConfigureAwait(false);
}
else
{
Expand Down Expand Up @@ -267,7 +319,13 @@ public async Task<bool> EnsureCreatedAsync(DbContext context, CancellationToken
if (!TestEnvironment.UseTokenCredential)
{
var cosmosClientWrapper = context.GetService<ICosmosClientWrapper>();
return await cosmosClientWrapper.CreateDatabaseIfNotExistsAsync(null, cancellationToken).ConfigureAwait(false);
var r = await cosmosClientWrapper.CreateDatabaseIfNotExistsAsync(null, cancellationToken).ConfigureAwait(false);
if (r)
{
_createdDatabases?.Add(Name);
}

return r;
}

var databaseAccount = await GetDBAccount(cancellationToken).ConfigureAwait(false);
Expand All @@ -293,7 +351,8 @@ public async Task<bool> EnsureCreatedAsync(DbContext context, CancellationToken
{
sqlDatabaseCreateUpdateContent.Options = new CosmosDBCreateUpdateConfig
{
Throughput = modelThroughput.Throughput, AutoscaleMaxThroughput = modelThroughput.AutoscaleMaxThroughput
Throughput = modelThroughput.Throughput,
AutoscaleMaxThroughput = modelThroughput.AutoscaleMaxThroughput
};
}

Expand Down Expand Up @@ -349,7 +408,8 @@ public override async Task CleanAsync(DbContext context, bool createTables = tru

if (!TestEnvironment.UseTokenCredential)
{
created = await context.Database.EnsureCreatedAsync().ConfigureAwait(false);
created = await ContextEnsureCreated(context).ConfigureAwait(false);

if (!created)
{
await SeedAsync(context).ConfigureAwait(false);
Expand Down Expand Up @@ -406,7 +466,8 @@ private async Task CreateContainersAsync(DbContext context)
{
content.Options = new CosmosDBCreateUpdateConfig
{
AutoscaleMaxThroughput = container.Throughput.AutoscaleMaxThroughput, Throughput = container.Throughput.Throughput
AutoscaleMaxThroughput = container.Throughput.AutoscaleMaxThroughput,
Throughput = container.Throughput.Throughput
};
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -86,11 +86,11 @@ public virtual Task subtract_and_TotalDays()
return AssertQuery(ss => ss.Set<BasicTypesEntity>().Where(o => (o.DateTime - date).TotalDays > 365));
}

[ConditionalFact]
[ConditionalFact(Skip = "Locale")]
public virtual Task Parse_with_constant()
=> AssertQuery(ss => ss.Set<BasicTypesEntity>().Where(o => o.DateTime == DateTime.Parse("5/4/1998 15:30:10 PM")));

[ConditionalFact]
[ConditionalFact(Skip = "Locale")]
public virtual Task Parse_with_parameter()
{
var date = "5/4/1998 15:30:10 PM";
Expand Down
Loading