Jadwal permintaan

Aktivasi biji-bijian memiliki model eksekusi utas tunggal . Secara default, mereka memproses setiap permintaan dari awal hingga selesai sebelum permintaan berikutnya dapat mulai diproses. Dalam beberapa keadaan, mungkin diinginkan agar aktivasi memproses permintaan lain sementara satu permintaan menunggu operasi asinkron selesai. Untuk alasan ini dan alasan lainnya, Orleans memberi Anda beberapa kontrol atas perilaku penyelingan permintaan, yang dijelaskan dalam bagian Reentrancy. Berikut ini adalah contoh penjadwalan permintaan non-reentrant, yang merupakan perilaku default di Orleans.

Pertimbangkan definisi berikut PingGrain :

public interface IPingGrain : IGrainWithStringKey
{
    Task Ping();
    Task CallOther(IPingGrain other);
}

public class PingGrain : Grain, IPingGrain
{
    private readonly ILogger<PingGrain> _logger;

    public PingGrain(ILogger<PingGrain> logger) => _logger = logger;

    public Task Ping() => Task.CompletedTask;

    public async Task CallOther(IPingGrain other)
    {
        _logger.LogInformation("1");
        await other.Ping();
        _logger.LogInformation("2");
    }
}

Dua butir jenis PingGrain terlibat dalam contoh kami, A dan B. Pemanggil melakukan panggilan berikut:

var a = grainFactory.GetGrain("A");
var b = grainFactory.GetGrain("B");
await a.CallOther(b);

Diagram penjadwalan reentransi.

Alur eksekusi adalah sebagai berikut:

  1. Panggilan tiba di A, yang mencatat "1" lalu mengeluarkan panggilan ke B.
  2. B segera kembali dari Ping() kembali ke A.
  3. A mencatat"2" dan mengembalikan ke pemanggil asli.

Meskipun A menunggu panggilan ke B, A tidak dapat memproses permintaan masuk apa pun. Akibatnya, jika A dan B saling memanggil secara bersamaan, mereka mungkin mengalami kebuntuan sambil menunggu panggilan tersebut selesai. Berikut adalah contohnya, berdasarkan panggilan yang dikeluarkan klien berikut:

var a = grainFactory.GetGrain("A");
var b = grainFactory.GetGrain("B");

// A calls B at the same time as B calls A.
// This might deadlock, depending on the non-deterministic timing of events.
await Task.WhenAll(a.CallOther(b), b.CallOther(a));

Kasus 1: Panggilan tidak mengalami deadlock

Diagram penjadwalan ulang tanpa kebuntuan.

Dalam contoh ini:

  1. Panggilan Ping() dari A tiba di B sebelum CallOther(a) panggilan tiba di B.
  2. Oleh karena itu, B memproses Ping() panggilan sebelum CallOther(a) panggilan.
  3. Karena B memproses Ping() panggilan, A dapat kembali ke pemanggil.
  4. Ketika B mengeluarkan panggilannya Ping() ke A, A masih sibuk mencatat pesannya ("2"), jadi panggilan harus menunggu durasi singkat, tetapi segera dapat diproses.
  5. A memproses Ping() panggilan dan mengembalikannya ke B, yang kemudian kembali ke pemanggil asli.

Pertimbangkan serangkaian peristiwa yang kurang beruntung di mana kode yang sama mengakibatkan kebuntuan karena waktu yang sedikit berbeda.

Kasus 2: Kebuntuan pada panggilan

Diagram penjadwalan ulang dengan kebuntuan.

Dalam contoh ini:

  1. Panggilan CallOther tiba di butir masing-masing dan diproses secara bersamaan.
  2. Kedua log biji-bijian "1" dan lanjutkan ke await other.Ping().
  3. Karena kedua butir masih sibuk (memproses CallOther permintaan, yang belum selesai), karena itulah Ping() permintaan harus menunggu.
  4. Setelah beberapa saat, Orleans menentukan bahwa panggilan telah habis waktu, dan setiap Ping() panggilan menghasilkan pengecualian yang dilemparkan.
  5. Isi CallOther metode tidak menangani pengecualian, dan menggelegak hingga pemanggil asli.

Bagian berikut menjelaskan cara mencegah kebuntuan dengan memungkinkan beberapa permintaan untuk menggabungkan eksekusinya.

Masuknya kembali

Peringatan

Reentrancy adalah fitur canggih yang membutuhkan pemahaman yang kuat tentang konsep konkurensi. Penggunaan yang salah dapat menyebabkan kondisi ras, kerusakan status, atau bug halus yang sulit didiagnosis. Pastikan Anda memahami implikasi sebelum mengaktifkan reentransi pada biji-bijian Anda.

Orleans mengatur default ke alur eksekusi yang aman di mana status internal grain tidak dimodifikasi secara bersamaan oleh beberapa permintaan. Modifikasi bersamaan mempersulit logika dan menempatkan beban yang lebih besar pada Anda, pengembang. Perlindungan terhadap bug konkurensi ini memiliki biaya, terutama keaktifan: pola panggilan tertentu dapat menyebabkan kebuntuan, seperti yang dibahas sebelumnya. Salah satu cara untuk menghindari kebuntuan adalah dengan memastikan panggilan biji-bijian tidak pernah membentuk siklus. Seringkali, sulit untuk menulis kode yang bebas siklus dan dijamin tidak mengalami kebuntuan. Menunggu setiap permintaan berjalan dari awal hingga selesai sebelum memproses yang berikutnya juga dapat merusak performa. Misalnya, secara default, jika metode grain melakukan permintaan asinkron ke layanan database, grain menjeda eksekusi permintaan hingga respons database tiba.

Masing-masing kasus ini dibahas di bagian berikut. Untuk alasan ini, Orleans menyediakan opsi untuk mengizinkan beberapa atau semua permintaan dijalankan secara bersamaan, menyelingi eksekusi mereka. Dalam Orleans, kami merujuk pada isu seperti reentrancy (keterulangan) atau interleaving (penyisipan). Dengan menjalankan permintaan secara bersamaan, biji-bijian yang melakukan operasi asinkron dapat memproses lebih banyak permintaan dalam waktu yang lebih singkat.

Beberapa permintaan dapat diselingi dalam kasus berikut:

Tabel berikut ini meringkas semua opsi reentransi yang tersedia:

Option Ruang lingkup Deskripsi
ReentrantAttribute Kelas biji-bijian Semua metode dalam biji-bijian dapat dengan bebas saling berhubungan.
AlwaysInterleaveAttribute Metode antarmuka Metode yang ditandai selalu berinteraksi dengan permintaan lain, dan permintaan lain dapat berinteraksi dengannya.
ReadOnlyAttribute Metode antarmuka Metode ini tidak mengubah status biji-bijian dan dapat berjalan bersamaan dengan metode lain ReadOnlyAttribute .
MayInterleaveAttribute Kelas biji-bijian Metode predikat menentukan, untuk setiap panggilan, apakah permintaan tertentu harus digabungkan.
AllowCallChainReentrancy() Lokasi Pemanggilan Memungkinkan reentransi selama durasi rantai panggilan yang mencakup pemanggil yang berada lebih jauh ke dalam rantai, memberikan kontrol terperinci atas tempat reentransi diaktifkan.

Dengan masuknya kembali, kasus berikut menjadi eksekusi yang valid, menghapus kemungkinan kebuntuan yang dijelaskan di atas.

Kasus 3: Biji-bijian atau metode bersifat reentrant

Diagram penjadwalan ulang reentri dengan butir atau metode yang bersifat reentri.

Dalam contoh ini, grain A dan B dapat saling memanggil secara bersamaan tanpa potensi kebuntuan penjadwalan permintaan karena kedua grain tersebut bersifat reentrant. Bagian berikut memberikan penjelasan lebih detail tentang reentransi.

Biji-bijian berpola ulang

Anda dapat menandai Grain kelas implementasi dengan ReentrantAttribute untuk menunjukkan bahwa permintaan yang berbeda dapat diselingi dengan mudah.

Dengan kata lain, aktivasi masuk kembali mungkin mulai memproses permintaan lain sementara permintaan sebelumnya belum selesai. Eksekusi masih terbatas pada satu utas, sehingga aktivasi menjalankan satu putaran pada satu waktu, dan setiap putaran dijalankan untuk memenuhi satu permintaan aktivasi.

Kode biji-bijian masuk kembali tidak pernah menjalankan beberapa bagian kode biji-bijian secara paralel (eksekusi selalu berurutan tunggal), tetapi biji-bijian masuk kembali mungkin melihat eksekusi kode untuk permintaan yang berbeda yang saling terkait. Artinya, kelanjutan dari permintaan yang berbeda dapat saling berselang-seling.

Misalnya, seperti yang ditunjukkan dalam kode pseudo berikut, pertimbangkan bahwa Foo dan Bar merupakan dua metode dari kelas biji-bijian yang sama:

Task Foo()
{
    await task1;    // line 1
    return Do2();   // line 2
}

Task Bar()
{
    await task2;   // line 3
    return Do2();  // line 4
}

Jika butir ini ditandai ReentrantAttribute, eksekusi Foo dan Bar dapat saling mengait.

Misalnya, urutan eksekusi berikut dimungkinkan:

Baris 1, baris 3, baris 2 dan baris 4. Artinya, giliran dari permintaan yang berbeda saling bergantian.

Jika proses tidak dapat dimulai ulang, satu-satunya urutan eksekusi yang mungkin adalah: baris 1, baris 2, baris 3, baris 4 ATAU: baris 3, baris 4, baris 1, baris 2 (permintaan baru tidak dapat dimulai sebelum yang sebelumnya selesai).

Tradeoff utama ketika memilih antara biji-bijian reentrant dan non-reentrant adalah kompleksitas kode yang diperlukan untuk membuat interleaving bekerja dengan benar dan kesulitan penalaran tentang hal itu.

Dalam kasus sepele di mana biji-bijian tanpa status dan logikanya sederhana, menggunakan lebih sedikit biji-bijian masuk kembali (tetapi tidak terlalu sedikit, memastikan semua utas perangkat keras digunakan) umumnya harus sedikit lebih efisien.

Jika kode lebih kompleks, menggunakan sejumlah besar komponen non-reentrant, bahkan jika sedikit kurang efisien secara keseluruhan, mungkin dapat menghemat Anda dari kesulitan signifikan dalam proses debugging masalah interleaving yang tidak jelas.

Pada akhirnya, jawabannya tergantung pada spesifikasi aplikasi.

Metode interleaving

Metode antarmuka butir yang ditandai dengan AlwaysInterleaveAttribute selalu dapat memasukkan permintaan lain dan selalu dapat diselingi oleh permintaan lain, bahkan permintaan untuk metode selain AlwaysInterleaveAttribute.

Pertimbangkan contoh berikut:

public interface ISlowpokeGrain : IGrainWithIntegerKey
{
    Task GoSlow();

    [AlwaysInterleave]
    Task GoFast();
}

public class SlowpokeGrain : Grain, ISlowpokeGrain
{
    public async Task GoSlow()
    {
        await Task.Delay(TimeSpan.FromSeconds(10));
    }

    public async Task GoFast()
    {
        await Task.Delay(TimeSpan.FromSeconds(10));
    }
}

Pertimbangkan alur panggilan yang dimulai oleh permintaan klien berikut:

var slowpoke = client.GetGrain<ISlowpokeGrain>(0);

// A. This will take around 20 seconds.
await Task.WhenAll(slowpoke.GoSlow(), slowpoke.GoSlow());

// B. This will take around 10 seconds.
await Task.WhenAll(slowpoke.GoFast(), slowpoke.GoFast(), slowpoke.GoFast());

Panggilan ke GoSlow tidak terjalin, sehingga sekitar 20 detik adalah total waktu eksekusi dari dua panggilan GoSlow. Di sisi lain, GoFast ditandai AlwaysInterleaveAttribute. Ketiga panggilan ke dalamnya dijalankan secara bersamaan, menyelesaikan total sekitar 10 detik alih-alih membutuhkan setidaknya 30 detik.

Metode hanya baca

Ketika metode grain tidak mengubah keadaan grain, aman untuk dijalankan bersamaan dengan permintaan lain. ReadOnlyAttribute menunjukkan bahwa suatu metode tidak memodifikasi keadaan objek grain. Menandai metode sebagai ReadOnlyAttribute memungkinkan Orleans untuk memproses permintaan Anda secara bersamaan dengan permintaan lain ReadOnlyAttribute , yang dapat secara signifikan meningkatkan performa aplikasi Anda. Pertimbangkan contoh berikut:

public interface IMyGrain : IGrainWithIntegerKey
{
    Task<int> IncrementCount(int incrementBy);

    [ReadOnly]
    Task<int> GetCount();
}

Metode GetCount tidak mengubah status grain, sehingga ditandai ReadOnlyAttribute. Pemanggil yang menunggu pemanggilan metode ini tidak diblokir oleh permintaan lain ReadOnlyAttribute ke grain, dan metode ini mengembalikan hasilnya segera.

Reentri pada rantai panggilan

Jika grain memanggil metode pada grain lain, yang kemudian memanggil kembali ke grain asli, panggilan dapat menyebabkan macet kecuali jika panggilan tersebut bersifat reentrant. Anda dapat mengaktifkan reentransi pada setiap lokasi panggilan menggunakan reentransi rantai panggilan. Untuk mengaktifkan reentransi rantai panggilan, panggil metode AllowCallChainReentrancy(). Metode ini mengembalikan nilai yang mendukung panggilan ulang oleh penelepon di rantai panggilan hingga nilai tersebut dibuang. Ini termasuk masuknya kembali dari biji-bijian yang memanggil metode itu sendiri. Pertimbangkan contoh berikut:

public interface IChatRoomGrain : IGrainWithStringKey
{
    ValueTask OnJoinRoom(IUserGrain user);
}

public interface IUserGrain : IGrainWithStringKey
{
    ValueTask JoinRoom(string roomName);
    ValueTask<string> GetDisplayName();
}

public class ChatRoomGrain : Grain<List<(string DisplayName, IUserGrain User)>>, IChatRoomGrain
{
    public async ValueTask OnJoinRoom(IUserGrain user)
    {
        var displayName = await user.GetDisplayName();
        State.Add((displayName, user));
        await WriteStateAsync();
    }
}

public class UserGrain : Grain, IUserGrain
{
    public ValueTask<string> GetDisplayName() => new(this.GetPrimaryKeyString());
    public async ValueTask JoinRoom(string roomName)
    {
        // This prevents the call below from triggering a deadlock.
        using var scope = RequestContext.AllowCallChainReentrancy();
        var roomGrain = GrainFactory.GetGrain<IChatRoomGrain>(roomName);
        await roomGrain.OnJoinRoom(this.AsReference<IUserGrain>());
    }
}

Dalam contoh sebelumnya, UserGrain.JoinRoom(roomName) memanggil ChatRoomGrain.OnJoinRoom(user), yang mencoba memanggil UserGrain.GetDisplayName() kembali untuk mendapatkan nama pengguna yang ditampilkan. Karena rantai panggilan ini melibatkan siklus, hal ini mengakibatkan kebuntuan jika UserGrain tidak mengizinkan masuknya kembali menggunakan salah satu mekanisme yang didukung yang dibahas dalam artikel ini. Dalam hal ini, kami menggunakan AllowCallChainReentrancy(), yang hanya roomGrain memungkinkan untuk memanggil kembali ke UserGrain. Ini memberi Anda kontrol yang lebih rinci atas di mana dan bagaimana reentrancy diaktifkan.

Jika Anda mencegah kebuntuan dengan menganotasi deklarasi GetDisplayName() metode di IUserGrain sebagai AlwaysInterleaveAttribute gantinya, Anda akan mengizinkan unit apapun untuk menggabungkan panggilan GetDisplayName dengan metode lainnya. Dengan menggunakan AllowCallChainReentrancy, Anda hanyaroomGrain memberikan izin untuk memanggil metode pada UserGrain, dan hanya sampai scope dihapus.

Menonaktifkan reentransi rantai panggilan

Anda juga dapat menekan masuknya kembali rantai panggilan menggunakan metode .SuppressCallChainReentrancy() Ini memiliki kegunaan terbatas untuk pengembang akhir tetapi penting untuk penggunaan internal oleh pustaka yang memperluas Orleans fungsionalitas inti, seperti streaming dan saluran siaran, untuk memastikan pengembang mempertahankan kontrol penuh atas kapan pengulangan rantai panggilan diaktifkan.

Reentrancy menggunakan predikat

Kelas biji-bijian dapat menentukan predikat untuk menentukan interleaving berdasarkan panggilan demi panggilan dengan memeriksa permintaan. Atribut [MayInterleave(string methodName)] ini menyediakan fungsionalitas ini. Argumen untuk atribut adalah nama fungsi statis dalam kelas grain. Metode ini menerima objek InvokeMethodRequest dan mengembalikan bool yang menunjukkan apakah permintaan dapat diselingi.

Berikut adalah contoh yang memungkinkan interleaving jika jenis argumen permintaan memiliki [Interleave] atribut :

[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct)]
public sealed class InterleaveAttribute : Attribute { }

// Specify the may-interleave predicate.
[MayInterleave(nameof(ArgHasInterleaveAttribute))]
public class MyGrain : Grain, IMyGrain
{
    public static bool ArgHasInterleaveAttribute(IInvokable req)
    {
        // Returning true indicates that this call should be interleaved with other calls.
        // Returning false indicates the opposite.
        return req.Arguments.Length == 1
            && req.Arguments[0]?.GetType()
                    .GetCustomAttribute<InterleaveAttribute>() != null;
    }

    public Task Process(object payload)
    {
        // Process the object.
    }
}