Implementasi model konkurensi berbasis CSP (Communicating Sequential Processes) seperti channel ala Go pada runtime .NET sering menimbulkan uji konkurensi yang tidak deterministik (flaky) dan kebuntuan (deadlock) pada runner CI. Masalah ini bukan disebabkan oleh kesalahan sintaksis, melainkan oleh gesekan semantik antara runtime coroutine G# dan ThreadPool bawaan .NET.

Akar Masalah: Ketidakcocokan Scheduling G# dan ThreadPool .NET

Lokal workstation developer umumnya memiliki 8 hingga 16 core fisik, sehingga pemblokiran goroutine/channel jarang memicu latensi penjadwalan. Sebaliknya, agen CI (seperti GitHub Actions standard runner) umumnya hanya memiliki 2 vCPU. Di lingkungan resource-constrained ini, dua kelemahan mendasar akan terekspos:

  • ThreadPool Thread-Injection Delay: Jika channel G# memblokir worker thread .NET alih-alih melepaskannya secara kooperatif, runtime .NET akan mengasumsikan thread sedang sibuk bekerja. Algoritma hill-climbing pada ThreadPool membatasi injeksi thread baru (sekitar 1 thread per 500ms). Konsumen channel yang tertahan di antrean eksekusi akan terlambat dijadwalkan, menyebabkan pengujian gagal akibat timeout arbitrer.
  • Ketidakselarasan Context Switching: Channel G# mengasumsikan scheduling kooperatif berbiaya rendah, sedangkan TaskScheduler .NET mengandalkan model work-stealing berbasis kuantum waktu OS. Ketika operasi kirim (send) dan terima (receive) terjadi secara masif, thread contention memicu race condition dalam pemeriksaan status buffer.

Strategi Deterministic Testing: Hapus Thread.Sleep

Penggunaan Thread.Sleep() atau Task.Delay() statis untuk menunggu channel menyelesaikan proses adalah antipola. Nilai waktu tunggu arbitrer selalu rapuh terhadap beban CPU CI.

Gunakan pengujian deterministik dengan prinsip:

  1. Bounded Synchronization: Gunakan CancellationTokenSource dengan durasi maksimal terikat pada setiap operasi channel, bukan sleep pasif.
  2. Explicit Completion Handshake: Pengujian harus mengamati sinyal terminasi channel (misalnya penutupan channel atau pengosongan buffer) menggunakan mekanisme event atau task completion.

Harness Pengujian: Deteksi Deadlock dan Task Leak

Berikut adalah implementasi harness pengujian unit yang menerapkan batas waktu eksplisit, verifikasi integritas buffer, dan pencegahan coroutine leak:

using System;
using System.Threading;
using System.Threading.Tasks;
using Xunit;

public sealed class DeterministicChannelFixture : IDisposable
{
    private readonly int _initialThreadCount;

    public DeterministicChannelFixture()
    {
        ThreadPool.GetAvailableThreads(out _initialThreadCount, out _);
    }

    public void Dispose()
    {
        // Verifikasi tidak ada thread pool starvation permanen
        ThreadPool.GetAvailableThreads(out int currentThreads, out _);
        if (currentThreads < _initialThreadCount - 2)
        {
            throw new InvalidOperationException("Task/Coroutine leak terdeteksi: Thread pool tidak kembali ke status baseline.");
        }
    }
}

public class ChannelConcurrencyTests : IClassFixture<DeterministicChannelFixture>
{
    [Fact]
    public async Task Channel_ProducerConsumer_MustProcessWithoutDeadlock()
    {
        // Setup batas waktu CI yang deterministik
        using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(3));
        var channel = new GSharpChannel<int>(capacity: 2);

        var producer = Task.Run(async () =>
        {
            for (int i = 0; i < 5; i++)
            {
                await channel.SendAsync(i, cts.Token);
            }
            channel.Close();
        }, cts.Token);

        var consumer = Task.Run(async () =>
        {
            int count = 0;
            while (await channel.WaitToReadAsync(cts.Token))
            {
                while (channel.TryReceive(out _))
                {
                    count++;
                }
            }
            return count;
        }, cts.Token);

        // Fail-fast bila terjadi deadlock sebelum timeout xUnit
        var completed = await Task.WhenAll(producer, consumer);
        
        Assert.Equal(5, consumer.Result);
        Assert.Equal(0, channel.CurrentBufferCount);
    }
}

Mitigasi Race Condition pada Interop C#

Saat menjembatani channel G# dengan library C# berbasis TaskCompletionSource<T> atau async event handler, hindari pemanggilan sinkron pada thread penangan channel. Hal ini memicu inversi dependensi eksekusi.

Gunakan opsi TaskCreationOptions.RunContinuationsAsynchronously untuk memutus rantai eksekusi pada thread yang sama:

public sealed class GSharpInteropBridge<T>
{
    private readonly TaskCompletionSource<T> _tcs = new(TaskCreationOptions.RunContinuationsAsynchronously);

    public void SignalFromCSharp(T value)
    {
        // RunContinuationsAsynchronously mencegah kelanjutan dieksekusi di context caller ini
        _tcs.TrySetResult(value);
    }

    public async Task TransferToChannelAsync(GSharpChannel<T> channel, CancellationToken ct)
    {
        T result = await _tcs.Task.WaitAsync(ct).ConfigureAwait(false);
        await channel.SendAsync(result, ct).ConfigureAwait(false);
    }
}

Verifikasi Regresi di Pipeline CI

Untuk memastikan kestabilan konkurensi di CI, konfigurasikan runner agar mendeteksi crash dan membatasi runtime pengujian yang hang secara terisolasi. Tambahkan konfigurasi --blame-hang pada CLI .NET:

dotnet test --configuration Release \
  --blame-hang \
  --blame-hang-timeout 10s \
  --blame-hang-dump-type mini \
  --logger "console;verbosity=normal"

Perintah ini memaksa runtime mengambil memory dump jika uji unit terhenti selama 10 detik, memungkinkan inspeksi thread stack trace untuk menganalisis titik temu deadlock tanpa memblokir seluruh workflow CI.