Skip to content
Open
Show file tree
Hide file tree
Changes from 4 commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
709a1bc
perf: share source session across workers
niteshvijay-ms Aug 18, 2026
f852240
refactor: make session ownership explicit
niteshvijay-ms Aug 18, 2026
a63bb45
refactor: retain session factory name
niteshvijay-ms Aug 18, 2026
8ea0f37
refactor: generalize session factory naming
niteshvijay-ms Aug 18, 2026
9dcc1d3
perf: allow twenty concurrent session opens
niteshvijay-ms Aug 18, 2026
341d8c7
fix: preserve source sessions during token rotation
niteshvijay-ms Aug 18, 2026
1f5c1e9
refactor: defer disposal of rotated sessions
niteshvijay-ms Aug 18, 2026
7a30d26
refactor: extend session disposal grace period
niteshvijay-ms Aug 18, 2026
9da9449
refactor: separate session rotation from token refresh
niteshvijay-ms Aug 18, 2026
25f2cc7
refactor: let session provider refresh itself
niteshvijay-ms Aug 18, 2026
fe1118a
refactor: create initial session in provider
niteshvijay-ms Aug 18, 2026
87d7000
refactor: capture immutable source settings
niteshvijay-ms Aug 18, 2026
7aade0e
refactor: use credential session factory object
niteshvijay-ms Aug 18, 2026
d5d95a9
perf: share source UDT registration cache
niteshvijay-ms Aug 19, 2026
f03fb5c
refactor: consolidate source session wrapper
niteshvijay-ms Aug 19, 2026
7df21ac
refactor: expose typed source session API
niteshvijay-ms Aug 19, 2026
c04db77
fix: harden shared session lifecycle
niteshvijay-ms Aug 19, 2026
9850625
style: expand method implementations
niteshvijay-ms Aug 19, 2026
2a6af55
Encapsulate token refresh in source sessions
niteshvijay-ms Aug 19, 2026
6000e47
Inline source token refresh lifecycle
niteshvijay-ms Aug 19, 2026
d8bd8f5
Centralize retriable operation execution
niteshvijay-ms Aug 19, 2026
974705c
Simplify shared session architecture
niteshvijay-ms Aug 19, 2026
b96e1b3
Group source session lifecycle state
niteshvijay-ms Aug 19, 2026
3971db5
Flatten source session wrapper state
niteshvijay-ms Aug 19, 2026
3cdff29
Use lock-free current session reads
niteshvijay-ms Aug 19, 2026
ca6a58b
Move AAD token ownership to source wrapper
niteshvijay-ms Aug 20, 2026
17403a4
Require explicit source AAD mode
niteshvijay-ms Aug 20, 2026
e484d13
Inline source session settings
niteshvijay-ms Aug 20, 2026
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
55 changes: 34 additions & 21 deletions CassandraMigrationProcessor/CassandraDriver/ISessionFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,24 +5,42 @@
namespace CassandraMigrationProcessor.CassandraDriver;

/// <summary>
/// Per-job session factory. Encapsulates everything required to mint a
/// new source or target <see cref="ISession"/> (job credentials, logger,
/// optional token refresh manager) so that consumers — primarily
/// <see cref="DataTransfer.PageReader"/> and
/// <see cref="DataTransfer.PageWriter"/> — depend on a single
/// abstraction instead of being threaded the raw <see cref="Job"/> and
/// <see cref="TokenRefreshManager"/> separately. This keeps worker-side
/// classes focused on data movement and makes it possible to swap the
/// connection wiring in tests without touching their constructors.
/// Creates worker-owned sessions. The consumer determines the session role;
/// job-owned shared sessions are passed directly instead of using this factory.
/// </summary>
public interface ISessionFactory
{
/// <summary>Mint a new keyspace-agnostic source-cluster session.</summary>
ISession CreateSourceSession();
/// <summary>Mint a new keyspace-agnostic session.</summary>
Task<ISession> CreateSessionAsync();
}

/// <summary>
/// Limits simultaneous session opens. This prevents high-worker jobs from
/// creating a connection storm during startup.
/// </summary>
public sealed class GatedSessionFactory : ISessionFactory
{
private readonly ISessionFactory _inner;
private readonly SemaphoreSlim _creationGate = new(2, 2);

/// <summary>Mint a new keyspace-agnostic target-cluster session. Async because
/// target credential discovery may go through ARM.</summary>
Task<ISession> CreateTargetSessionAsync();
public GatedSessionFactory(ISessionFactory inner)
{
_inner = inner ?? throw new ArgumentNullException(nameof(inner));
}

public async Task<ISession> CreateSessionAsync()
{
await _creationGate.WaitAsync().ConfigureAwait(false);
try
{
return await _inner.CreateSessionAsync()
.ConfigureAwait(false);
}
finally
{
_creationGate.Release();
}
}
}

/// <summary>
Expand All @@ -34,18 +52,13 @@ public sealed class JobSessionFactory : ISessionFactory
{
private readonly MigrationLog _log;
private readonly Job _job;
private readonly TokenRefreshManager? _tokenRefreshManager;

public JobSessionFactory(MigrationLog log, Job job, TokenRefreshManager? tokenRefreshManager)
public JobSessionFactory(MigrationLog log, Job job)
{
_log = log;
_job = job;
_tokenRefreshManager = tokenRefreshManager;
}

public ISession CreateSourceSession()
=> CassandraClientFactory.CreateSourceSession(_log, _job, _tokenRefreshManager);

public Task<ISession> CreateTargetSessionAsync()
public Task<ISession> CreateSessionAsync()
=> CassandraClientFactory.CreateTargetSessionAsync(_log, _job);
}
3 changes: 1 addition & 2 deletions CassandraMigrationProcessor/DataTransfer/DataCopyWorker.cs
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ public async Task RunAsync(PipelineContext ctx)
Partition? current = null;
try
{
reader = await PageReader.CreateAsync(_workerLog, ctx.SessionFactory, ctx.ReaderConfig, _ct);
reader = await PageReader.CreateAsync(_workerLog, ctx.SourceSession, ctx.ReaderConfig, _ct);
Comment thread
niteshvijay1995 marked this conversation as resolved.
Outdated
writer = await PageWriter.CreateAsync(_workerLog, ctx.SessionFactory, ctx.WriterConfig, _ct);

while (!_ct.IsCancellationRequested
Expand Down Expand Up @@ -159,7 +159,6 @@ public async Task RunAsync(PipelineContext ctx)
// instant any worker exits. Channel completion is driven
// by the orchestrator (offline) or _cts.Cancel (fatal).
MigrationUtilities.SafeDispose(writer, "worker PageWriter");
MigrationUtilities.SafeDispose(reader, "worker PageReader");
}
}

Expand Down
9 changes: 7 additions & 2 deletions CassandraMigrationProcessor/DataTransfer/JobPipeline.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using Cassandra;
using CassandraMigrationProcessor.CassandraDriver;
using CassandraMigrationProcessor.Infrastructure;
using CassandraMigrationProcessor.Models;
Expand All @@ -20,7 +21,10 @@ internal sealed class JobPipeline : IDisposable, IAsyncDisposable
private readonly PartitionManager _partitions;
public PipelineContext Context { get; }

public JobPipeline(MigrationLog log, Job job, PipelineConfig pipelineConfig, JobPartitioning partitioning, TokenRefreshManager? tokenRefreshManager, JobControl control)
public JobPipeline(MigrationLog log, Job job, PipelineConfig pipelineConfig,
JobPartitioning partitioning, ISession sourceSession,
ISessionFactory sessionFactory,
JobControl control)
{
_log = log;
_pipelineConfig = pipelineConfig;
Expand All @@ -43,7 +47,8 @@ public JobPipeline(MigrationLog log, Job job, PipelineConfig pipelineConfig, Job

Context = new PipelineContext(
_partitions,
new JobSessionFactory(log, job, tokenRefreshManager),
sourceSession,
sessionFactory,
readerConfig,
writerConfig,
EnableReplay: enableReplay,
Expand Down
12 changes: 9 additions & 3 deletions CassandraMigrationProcessor/DataTransfer/MigrationJobRunner.cs
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,9 @@ public class MigrationJobRunner : IAsyncDisposable
/// Runner-wide source / target sessions opened once in
/// <see cref="CreateAsync"/> and reused across wildcard expansion,
/// schema provisioning, and partition discovery. Disposed in
/// <see cref="DisposeAsync"/>. Copy workers mint their own sessions
/// via <see cref="ISessionFactory"/> for throughput isolation.
/// <see cref="DisposeAsync"/>. Copy workers reuse the thread-safe source
/// session to avoid multiplying driver metadata topology/schema handshakes,
/// while retaining independent target sessions for write throughput.
/// For simulated runs the target session is a <see cref="NullSession"/>.
/// </summary>
private readonly ISession _sourceSession;
Expand Down Expand Up @@ -173,7 +174,12 @@ public async Task StartAsync()
var partitioning = await RunPartitioningPhaseAsync(
job, units, cancellationToken);

_pipeline = new JobPipeline(_log, job, _pipelineConfig, partitioning, _tokenRefreshManager, _control);
_pipeline = new JobPipeline(
_log, job, _pipelineConfig, partitioning,
_sourceSession,
new GatedSessionFactory(
new JobSessionFactory(_log, job)),
_control);
_pipeline.Start();

await RunCopyPhaseAsync(job, units, partitioning, cancellationToken);
Expand Down
12 changes: 5 additions & 7 deletions CassandraMigrationProcessor/DataTransfer/PageReader.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ internal record ReaderConfig(int PageSize, int MaxReadRetries, bool PreserveCell
/// cached per keyspace so the first partition for each table pays the
/// cost and subsequent partitions reuse it.
/// </summary>
internal class PageReader : IDisposable
internal class PageReader
{
private readonly WorkerLog _log;
private readonly CancellationToken _ct;
Expand All @@ -50,26 +50,24 @@ internal class PageReader : IDisposable
// hints parking a worker for minutes.
private const int MaxRetryDelayMs = 30_000;

private PageReader(WorkerLog log, ISessionFactory sessionFactory, ReaderConfig config, CancellationToken cancellationToken)
private PageReader(WorkerLog log, ISession sourceSession, ReaderConfig config, CancellationToken cancellationToken)
{
_log = log;
_ct = cancellationToken;
_pageSize = config.PageSize;
_maxReadRetries = config.MaxReadRetries;
_preserveCellTtl = config.PreserveCellTtlAndWritetime;
_useJsonCopy = config.UseJsonCopy;
_sourceSession = sessionFactory.CreateSourceSession();
_sourceSession = sourceSession;
}

public static Task<PageReader> CreateAsync(WorkerLog log,
ISessionFactory sessionFactory, ReaderConfig config,
ISession sourceSession, ReaderConfig config,
CancellationToken cancellationToken)
{
return Task.FromResult(new PageReader(log, sessionFactory, config, cancellationToken));
return Task.FromResult(new PageReader(log, sourceSession, config, cancellationToken));
}

public void Dispose() => MigrationUtilities.SafeDisposeSession(_sourceSession, "PageReader source session");

/// <summary>
/// Lazy, idempotent UDT registration for typed reads. The first typed
/// table registers every UDT in the keyspace because this reader can
Expand Down
5 changes: 3 additions & 2 deletions CassandraMigrationProcessor/DataTransfer/PageWriter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -60,11 +60,12 @@ private PageWriter(WorkerLog log, ISession targetSession,

public static async Task<PageWriter> CreateAsync(WorkerLog log, ISessionFactory sessionFactory, WriterConfig config, CancellationToken cancellationToken)
{
var targetSession = await sessionFactory.CreateTargetSessionAsync();
var targetSession = await sessionFactory.CreateSessionAsync();
return new PageWriter(log, targetSession, config, cancellationToken);
}

public void Dispose() => MigrationUtilities.SafeDisposeSession(_targetSession, "PageWriter target session");
public void Dispose()
=> MigrationUtilities.SafeDisposeSession(_targetSession, "PageWriter target session");

private Task<IRowWriteStrategy> GetStrategyAsync(Partition partition)
{
Expand Down
6 changes: 4 additions & 2 deletions CassandraMigrationProcessor/DataTransfer/PipelineContext.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using Cassandra;
using CassandraMigrationProcessor.CassandraDriver;

namespace CassandraMigrationProcessor.DataTransfer;
Expand All @@ -6,14 +7,15 @@ namespace CassandraMigrationProcessor.DataTransfer;
/// Shared (job-wide) state passed to every worker. Holds the
/// <see cref="DataTransfer.PartitionManager"/> that all tables seed into and
/// every worker pulls from (and which owns the cooldown scheduler for
/// delayed recycles), the connection capability used by readers and
/// writers, reader / writer tunables, the replay configuration knobs, and
/// delayed recycles), the shared source session, worker session factory,
/// reader / writer tunables, the replay configuration knobs, and
/// the unified <see cref="JobControl"/> (cancellation + first-fault).
/// Per-table state is resolved through <see cref="Partition"/>
/// pass-through accessors.
/// </summary>
internal record PipelineContext(
PartitionManager Partitions,
ISession SourceSession,
ISessionFactory SessionFactory,
ReaderConfig ReaderConfig,
WriterConfig WriterConfig,
Expand Down
4 changes: 2 additions & 2 deletions CassandraMigrationProcessor/Models/TableCopySpec.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ namespace CassandraMigrationProcessor.Models;
/// <summary>
/// Immutable description of a single table copy. Identifies the source
/// and target keyspace/table; runtime sessions are not threaded through
/// here — readers and writers open sessions via the job-wide
/// <c>ISessionFactory</c>.
/// here — readers use the job-wide source session and writers open
/// worker-owned sessions through <c>ISessionFactory</c>.
/// </summary>
public record TableCopySpec(
string KeyspaceName,
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ A .NET 9 Blazor Server web app that migrates data from **Azure Cosmos DB for Apa

- **Schema auto-sync** — discovers source keyspaces, tables, columns, clustering keys, and static columns; generates DDL on the target automatically.
- **Feed-range partitioned bulk copy** — splits each table into token-range chunks and copies them with configurable parallelism.
- **Per-worker Cassandra sessions** — each parallel worker maintains its own driver session for maximum throughput.
- **Efficient Cassandra sessions** — workers share the thread-safe source session while retaining independent target sessions for write throughput.
- **Checkpoint-based pause / resume** — stop at any time and continue later with zero data loss; state is persisted to disk.
- **Online mode (change feed)** — after the initial bulk copy, replays Cosmos DB change feed events to keep the target in sync until cutover.
- **Wildcard table selection** — migrate all tables in a keyspace with `keyspace.*` or pick individual tables.
Expand Down