-
Notifications
You must be signed in to change notification settings - Fork 59
Open
Description
When inserting millions of records using BulkInsertOptimizedAsync (event traces of the BPIC17) I noticed that the operation does not cancel even though properly using and passing a CancellationToken. Here a minimal reproducible example:
Docker DB:
version: "3.8"
services:
db:
image: postgres:16-alpine
container_name: bulk-insert-cancel-test-db
restart: unless-stopped
environment:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
POSTGRES_DB: db
ports:
- "45432:5432"
Code:
using System;
using System.Diagnostics;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
public class TableItem {
public Guid Id { get; set; }
public string Name { get; set; } = default!;
}
public class TestContext : DbContext {
public DbSet<TableItem> Table { get; set; }
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) {
if (!optionsBuilder.IsConfigured) {
var connectionString = "Host=localhost;Port=45432;Database=db;Username=postgres;Password=postgres";
optionsBuilder.UseNpgsql(connectionString);
}
}
protected override void OnModelCreating(ModelBuilder modelBuilder) {
modelBuilder.Entity<TableItem>(entity => {
entity.HasKey(e => e.Id);
entity.Property(e => e.Id).ValueGeneratedNever();
entity.Property(e => e.Name)
.IsRequired()
.HasMaxLength(256);
});
}
}
public static class ProgramBulkInsert {
public static async Task Main(string[] args) {
const int totalItems = 5_000_000;
Console.WriteLine("Ensuring database is created...");
await using (var setupContext = new TestContext()) {
await setupContext.Database.EnsureCreatedAsync();
await setupContext.Database.ExecuteSqlRawAsync($"TRUNCATE TABLE \"{nameof(TestContext.Table)}\";");
}
Console.WriteLine($"Generating {totalItems:N0} entities in memory...");
var items = Enumerable.Range(1, totalItems)
.Select(i => new TableItem {
Id = Guid.CreateVersion7(),
Name = $"Item #{i}"
})
.ToList();
Console.WriteLine("Entities generated.");
using var cts = new CancellationTokenSource();
var token = cts.Token;
var stopwatch = new Stopwatch();
_ = Task.Run(async () => {
await Task.Delay(TimeSpan.FromSeconds(2));
Console.WriteLine(">>> Requesting cancellation");
cts.Cancel();
stopwatch.Start();
});
Console.WriteLine("Starting BulkInsertOptimizedAsync with cancellation token...");
try {
await using var context = new TestContext();
_ = await context.BulkInsertOptimizedAsync(items, token);
stopwatch.Stop();
Console.WriteLine("BulkInsertOptimizedAsync COMPLETED without throwing.");
await using var verifyContext = new TestContext();
var countInDb = await verifyContext.Table.LongCountAsync();
Console.WriteLine($"Rows in DB after operation: {countInDb:N0}");
}
catch (OperationCanceledException) {
stopwatch.Stop();
Console.WriteLine("BulkInsertOptimizedAsync threw OperationCanceledException (was cancelled).");
Console.WriteLine($"Elapsed since Cancel: {stopwatch.Elapsed}");
}
catch (Exception) {
stopwatch.Stop();
Console.WriteLine("BulkInsertOptimizedAsync threw some other exception:");
}
Console.WriteLine($"CancellationToken.IsCancellationRequested = {token.IsCancellationRequested}");
}
}
Packages:
<ItemGroup>
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="10.0.0" />
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="10.0.0" />
<PackageReference Include="Z.EntityFramework.Extensions.EFCore" Version="10.105.0" />
</ItemGroup>
Metadata
Metadata
Assignees
Labels
No labels