Changeset 002cf5f


Ignore:
Timestamp:
09/16/26 22:59:59 (7 days ago)
Author:
Boris Gjorgjievski <boris@…>
Branches:
master
Children:
0496e6e
Parents:
1b20b22
Message:

Phase 8: connection pooling and concurrent-enrolment handling

Two enrolments for the same semester sent at once both pass the
"already enrolled" check, because each transaction reads the state from
before the other one wrote. UNIQUE (user_id, semester_id) is what
settles it, so EnrollAsync now catches the 23505 and returns the same
sentence instead of a 500.

Sizes the connection pool in the connection string (max 10, min 1)
rather than taking Npgsql's default of 100 - every physical connection
is one more channel through the SSH tunnel - and switches to
AddDbContextPool, which AppDbContext qualifies for since its only
constructor takes DbContextOptions.

docs/Ph8.md is the wiki page: the three transactional scenarios, the
isolation level and what it does not cover, and the pool settings with
the measured connection counts.

Files:
1 added
3 edited

Legend:

Unmodified
Added
Removed
  • iknow-api/Program.cs

    r1b20b22 r002cf5f  
    6666// Postgres type by name; PgEnumLabels covers the members whose label is not
    6767// simply the lowercased member name.
    68 builder.Services.AddDbContext<AppDbContext>(options =>
     68// AddDbContextPool reuses the DbContext instances themselves; the connections
     69// underneath them are pooled separately by Npgsql, sized in the connection
     70// string. Both matter here because every physical connection is one more
     71// channel through the SSH tunnel.
     72builder.Services.AddDbContextPool<AppDbContext>(options =>
    6973    options.UseNpgsql(
    7074        builder.Configuration.GetConnectionString("DefaultConnection"),
  • iknow-api/Services/Implementations/EnrollmentService.cs

    r1b20b22 r002cf5f  
    33using iknow_api.Models;
    44using Microsoft.EntityFrameworkCore;
     5using Npgsql;
    56
    67namespace iknow_api.Services
     
    220221                };
    221222            }
     223            catch (DbUpdateException ex) when (IsUniqueViolation(ex))
     224            {
     225                // Two enrolments for the same semester sent at the same time both
     226                // pass the check above, because each transaction reads the state
     227                // from before the other one wrote. UNIQUE (user_id, semester_id)
     228                // is what actually settles it, so the loser gets the same
     229                // sentence as if the check had caught it.
     230                await transaction.RollbackAsync();
     231                return Fail("You are already enrolled in that semester.");
     232            }
    222233            catch
    223234            {
     
    226237            }
    227238        }
     239
     240        /// <summary>23505 is unique_violation.</summary>
     241        private static bool IsUniqueViolation(DbUpdateException ex) =>
     242            ex.InnerException is PostgresException { SqlState: "23505" };
    228243
    229244        private static EnrollSemesterResultDto Fail(string message) =>
  • iknow-api/appsettings.Development.json.example

    r1b20b22 r002cf5f  
    77  },
    88  "ConnectionStrings": {
    9     "DefaultConnection": "Host=localhost;Port=9999;Database=db_202526z_va_prj_know2026;Username=db_202526z_va_prj_iknow2026_owner;Password=YOUR_DB_PASSWORD_HERE;Search Path=project;Include Error Detail=true"
     9    "DefaultConnection": "Host=localhost;Port=9999;Database=db_202526z_va_prj_know2026;Username=db_202526z_va_prj_iknow2026_owner;Password=YOUR_DB_PASSWORD_HERE;Search Path=project;Include Error Detail=true;Maximum Pool Size=10;Minimum Pool Size=1;Connection Idle Lifetime=300;Timeout=15"
    1010  },
    1111  "Jwt": {
Note: See TracChangeset for help on using the changeset viewer.