BulkLoad Write Path
Skip per-row WAL for mass loads โ one checkpoint-backed manifest pair makes the whole load atomic, not each row.
Status: ๐ง Partial ยท Visibility: Public ยท Level: ๐ฃ Advanced ยท Category: Durability Assumes: Durability Modes
๐ฏ What it solves
Typhon's default write path is latency-critical: every spawn/update/destroy emits a per-row WAL record and dirties
pages that only the background checkpoint can drain, throwing a backpressure timeout if it can't keep up. That's
the right trade-off for OLTP traffic; it's the wrong one for seeding a multi-million-entity world, importing a
dataset, or rebuilding a fixture. Every mature engine ships a second, throughput-first path for exactly this case
(SQL Server BULK_LOGGED, Postgres pg_bulkload) โ BulkLoad is Typhon's.
โ๏ธ How it works (in brief)
BeginBulkLoad opens an exclusive session that writes spawned/updated/destroyed entities straight to pages,
skipping per-row WAL entirely. The session is bracketed by two manifest records: BulkBegin at open, BulkEnd at
CompleteBulkLoad. CompleteBulkLoad is the durability barrier โ it drains every dirty page, forces a checkpoint
and waits for it, emits BulkEnd, and waits for that record to be durable before returning. Only then are the
session's entities visible to other transactions. Closing the session any other way (Dispose without
CompleteBulkLoad, or a crash) discards the entire session as if it never ran โ there is no partial result.
๐ป Usage
using var session = engine.BeginBulkLoad(new BulkLoadOptions
{
ProgressReporter = p => Console.WriteLine($"{p.EntitiesSpawned:N0} spawned ({p.ElapsedMilliseconds} ms)"),
ProgressBatchSize = 50_000,
CheckpointTimeout = TimeSpan.FromMinutes(10),
});
for (int i = 0; i < 4_000_000; i++)
{
var id = session.Spawn<ParticleArchetype>();
session.Update(id, new Position { X = i, Y = 0, Z = 0 });
}
try
{
session.CompleteBulkLoad(); // synchronous: drain + checkpoint + BulkEnd barrier before returning
}
catch (BulkLoadCheckpointTimeoutException)
{
session.CompleteBulkLoad(); // session is still open โ retry, or Dispose() to discard
}
// 4M entities now durable and visible to subsequent transactions.
| Option | Default | Effect |
|---|---|---|
ProgressReporter |
null |
Callback invoked every ProgressBatchSize operations |
ProgressBatchSize |
10_000 |
Operations between progress callbacks |
CheckpointTimeout |
5 min | Max wait in CompleteBulkLoad for the forced checkpoint; throws BulkLoadCheckpointTimeoutException and leaves the session open on expiry |
โ ๏ธ Guarantees & limits
- Exclusive โ only one
BulkLoadSessionper engine; a secondBeginBulkLoadwhile one is open throwsBulkSessionAlreadyActiveException. - Thread-affine โ only the thread that called
BeginBulkLoadmay call methods on the returned session. - No per-row WAL โ the only WAL traffic for the session is the
BulkBegin/BulkEndmanifest pair (plus unrelated TickFence activity);DurabilityMode/DurabilityOverridedon't apply inside a session. - Session-granularity atomicity, not per-row โ
CompleteBulkLoadreturning is the only success signal: every spawn/update/destroy in the session becomes durable and visible together, or (onDisposewithoutCompleteBulkLoad, or a crash) none of them do. - Concurrent readers unaffected โ regular
UnitOfWorks keep running during the session and see the pre-bulk MVCC snapshot; bulk-spawned entities aren't visible to them untilCompleteBulkLoadreturns. - Update/Destroy scope โ only target entities spawned earlier in the same session; mutating pre-existing entities requires the standard
UnitOfWorkpath. - No latency budget โ
CompleteBulkLoadblocks on a real checkpoint cycle;CheckpointTimeoutonly guards against it never finishing, it doesn't bound how long the call takes. - Partial status โ discarded-session page reclamation isn't wired yet: pages allocated by a session that's
Disposed withoutCompleteBulkLoad(or that crashes) stay marked occupied โ not user-visible, but not freed โ until a future recovery increment adds allocation tracking and explicit free.
๐งช Tests
- BulkLoadApiSurfaceTests โ session exclusivity/lifecycle,
BeginBulkLoad/Dispose/CompleteBulkLoadAPI surface - BulkLoadWriteTests โ no per-row WAL, exactly one
BulkBegin/BulkEndpair, entities invisible untilCompleteBulkLoad - BulkLoadRecoveryTests โ crash after
BulkBegindiscards the session; crash afterCompleteBulkLoadsurvives reopen
๐ Related
- Sibling entry: TyphonException Hierarchy & Catalog